SlideShare a Scribd company logo
1 of 105
Surpresas no uso
de Concorrência
    em by Marcos Toledo
        Rails
Marcos Renato
Baroni Toledo
  @mtoledo
Socram O’taner
Socram O’taner

Marcos Renato
O’taner
Email
“I’m trying to
unsubscribe but
it says email has
   already been
      taken”!
validates_uniquen
      ess_of
   # id :integer(4)
   # email :string(255)
   class User < ActiveRecord::Base
    validates_uniqueness_of :email
   end
WTF ..
  ?
Invalid Records!

User.find_by_email(“angry_customer@email.com”).valid?
# => false
Why?
Rails can’t
 guarantee
uniqueness!
Solutions?
Uniqueness
           Constraint!
class AddUniquenessIndexToUserEmail < ActiveRecord::Migration
 def self.up
   add_index :users, :email, :unique => true
 end

 def self.down
  remove_index :users, :email
 end
end
Succ
ess!
Another Email
Airbrake!
NoMethodError: undefined method `title' for
               nil:NilClass




       “nil post”?
:dependent
     => :destroy
class Comment < ActiveRecord::Base
 belongs_to :post
 validates_presence_of :post
end

class Post < ActiveRecord::Base
 has_many :comments, :dependent => :destroy
end
WTF ..
  ?
Orphan Models!

Comment.all.reject(&:post).size
# => 1
Why?
Rails can’t
 guarantee
dependency
   either!
Solutions?
Database Foreign
     Keys!
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
Succ
ess!
1 - Noob
“Use database
 constraints”
O’taner
ruby 100% CPU!
Solutions?
Professional
Debugging
ssh server
killall ruby
Succ
ess!
WET Code
Infinite Loop
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
WTF ..
  ?
Transaction
Isolation Level

  “I” in ACID
1 - READ
UNCOMMITTED
2 - READ
COMMITTED
3 - REPEATABLE
READ
Postgre
READ-COMMITTED
     MySQL
REPEATABLE-READ
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)
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)
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)
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)
Solutions?
READ-
    COMMITTED!
              /etc/my.cnf
[mysqld]
transaction-isolation = READ-
COMMITTED




                                 Just like
                                postgre ;)
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)
Succ
ess!
2 - Beginner
“Use READ-
COMMITTED”
O’taner
Production
             Console
> 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}
WTF ..
  ?
# login :string(255)
# options :text
class User < ActiveRecord::Base
 serialize :options

 def activate
  self.options = {:active => true}
  self.save
 end
end
No ‘:active =>
    false’!
Why?
Dirty Checking
> 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}
Serialize is
ALWAYS dirty!
// u1.update_attributes :options => {:active => true}
UPDATE `users`
SET `options` = '--- n:active: truen'
WHERE (`users`.`id` = 1)

// u2.update_attributes :login => "mtoledo"
UPDATE `users`
SET `login` = 'mtoledo',
    `options` = '--- n:active: falsen'
WHERE (`users`.`id` = 1)
Solutions?
Options Model
# id         :integer(4)
# user_id       :integer(4)
# lock_version :integer(4)
# options       :text
class UserOptions < ActiveRecord::Base
 serialize :options
 belongs_to :user
end
Succ
ess!
3 - Half Mouth
“Use an Options
     Model”
O’taner
production.log

ERROR 1213 (40001): Deadlock found when trying to get
            lock;try restarting transaction
DAMN!
Pessimistic Locks!
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
WTF ..
  ?
Transac
 Transaction 1
SELECT * FOR
                         tion 2
UPDATE
FROM `comments`
WHERE `id` = 1        SELECT * FOR
                      UPDATE
        locked..      FROM `posts`
                      WHERE `id` = 1
SELECT * FOR
UPDATE
FROM `posts`       DEADLOCK!
WHERE `id` = 1
                      SELECT * FOR
                      UPDATE
                      FROM `comments`
                      WHERE `id` = 1
Why?
Transactions lock
   on update
Transac
  Transaction 1
UPDATE
                             tion 2
`comments`
SET `title` = “Foo”
WHERE `id` = 1
                         UPDATE `posts`
                         SET `title` = “bar”
          locked..       WHERE `id` = 1

UPDATE `posts`
SET `title` = “bar”
WHERE `id` = 1
                      DEADLOCK!
                         UPDATE
                         `comments`
                         SET `title` = “foo”
                         WHERE `id` = 1
Callbacks =
Transactions
Solutions?
Update
 dependents
before parents!
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
Avoid State
Propagation
Avoids updating
 all comments
class Comment < ActiveRecord::Base
 def visible?
   post.published? && self.published?
 end
end
Succ
ess!
4 - Advanced
“Acquire locks
 consistently”
O’taner
“There is no safe way to use
                     Thread#kill or
                    Thread#raise.”
http://blog.headius.com/2008/02/rubys-threadraise-threadkill-timeoutrb.html
Killing ruby (MRI)
      threads
 - bluepill
 - god
 - monit
 - you
 and me
Simple
      Transaction
class Order
 belongs_to :user
 after_create :mark_user_as_customer

 def mark_user_as_customer
  user.customer = true
  user.save
 end
end
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>
WTF ..
  ?
If you kill your
      Ruby
(MRI) Threads ..
ActiveRecord will
     partially
  commit your
  transactions!
Why?
ActiveRecord
    transaction
 (version for kids)
def transaction
 yield
rescue Exception => e
 rollback
ensure
 commit unless rolledback?
end
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
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
It commits!
def transaction
 yield # thread killed inside yield
rescue Exception => e
 rollback # nothing is raised
ensure
 commit unless rolledback?
end
Solutions?
DON’T Kill Your
  Threads!

           Headius Approv
If you do, don’t
use ActiveRecord
+ Threads + MRI
5 Record Certified Enterprise
 Active
         - ARCECA
      “Don’t use
       Concurrency Architect


Thread#kill/raise ”
Succ
ess!
Recap please!
1 - Noob
“Use database
 constraints”
2 - Beginner
“Use READ-
COMMITTED”
3 - Half Mouth
“Use an Options
     Model”
4 - Advanced
“Acquire locks
 consistently”
5 Record Certified Enterprise
 Active
         - ARCECA
      “Don’t use
       Concurrency Architect


Thread#kill/raise ”
That’s all! Thank
      You!
Questions?
Slides @mtoledo

More Related Content

What's hot

Beyond Breakpoints: Advanced Debugging with XCode
Beyond Breakpoints: Advanced Debugging with XCodeBeyond Breakpoints: Advanced Debugging with XCode
Beyond Breakpoints: Advanced Debugging with XCodeAijaz Ansari
 
Active Record Query Interface (1), Season 2
Active Record Query Interface (1), Season 2Active Record Query Interface (1), Season 2
Active Record Query Interface (1), Season 2RORLAB
 
The Ring programming language version 1.2 book - Part 32 of 84
The Ring programming language version 1.2 book - Part 32 of 84The Ring programming language version 1.2 book - Part 32 of 84
The Ring programming language version 1.2 book - Part 32 of 84Mahmoud Samir Fayed
 
Optimizing Queries with Explain
Optimizing Queries with ExplainOptimizing Queries with Explain
Optimizing Queries with ExplainMYXPLAIN
 
The Ring programming language version 1.8 book - Part 34 of 202
The Ring programming language version 1.8 book - Part 34 of 202The Ring programming language version 1.8 book - Part 34 of 202
The Ring programming language version 1.8 book - Part 34 of 202Mahmoud Samir Fayed
 
The Ring programming language version 1.6 book - Part 31 of 189
The Ring programming language version 1.6 book - Part 31 of 189The Ring programming language version 1.6 book - Part 31 of 189
The Ring programming language version 1.6 book - Part 31 of 189Mahmoud Samir Fayed
 
The Ring programming language version 1.5.1 book - Part 27 of 180
The Ring programming language version 1.5.1 book - Part 27 of 180The Ring programming language version 1.5.1 book - Part 27 of 180
The Ring programming language version 1.5.1 book - Part 27 of 180Mahmoud Samir Fayed
 
Scaling MySQL Strategies for Developers
Scaling MySQL Strategies for DevelopersScaling MySQL Strategies for Developers
Scaling MySQL Strategies for DevelopersJonathan Levin
 
The Ring programming language version 1.4.1 book - Part 8 of 31
The Ring programming language version 1.4.1 book - Part 8 of 31The Ring programming language version 1.4.1 book - Part 8 of 31
The Ring programming language version 1.4.1 book - Part 8 of 31Mahmoud Samir Fayed
 
The Ring programming language version 1.7 book - Part 32 of 196
The Ring programming language version 1.7 book - Part 32 of 196The Ring programming language version 1.7 book - Part 32 of 196
The Ring programming language version 1.7 book - Part 32 of 196Mahmoud Samir Fayed
 
ActiveRecord Query Interface (2), Season 2
ActiveRecord Query Interface (2), Season 2ActiveRecord Query Interface (2), Season 2
ActiveRecord Query Interface (2), Season 2RORLAB
 
Introduction to SQLAlchemy ORM
Introduction to SQLAlchemy ORMIntroduction to SQLAlchemy ORM
Introduction to SQLAlchemy ORMJason Myers
 
Rapid and Scalable Development with MongoDB, PyMongo, and Ming
Rapid and Scalable Development with MongoDB, PyMongo, and MingRapid and Scalable Development with MongoDB, PyMongo, and Ming
Rapid and Scalable Development with MongoDB, PyMongo, and MingRick Copeland
 
The Ring programming language version 1.4.1 book - Part 13 of 31
The Ring programming language version 1.4.1 book - Part 13 of 31The Ring programming language version 1.4.1 book - Part 13 of 31
The Ring programming language version 1.4.1 book - Part 13 of 31Mahmoud Samir Fayed
 
32373 uploading-php-shell-through-sql-injection
32373 uploading-php-shell-through-sql-injection32373 uploading-php-shell-through-sql-injection
32373 uploading-php-shell-through-sql-injectionAttaporn Ninsuwan
 
Maven + Jsf + Richfaces + Jxl + Jdbc - Complete Code Example
Maven + Jsf + Richfaces + Jxl + Jdbc - Complete Code ExampleMaven + Jsf + Richfaces + Jxl + Jdbc - Complete Code Example
Maven + Jsf + Richfaces + Jxl + Jdbc - Complete Code ExampleNikhil Bhalwankar
 

What's hot (20)

Beyond Breakpoints: Advanced Debugging with XCode
Beyond Breakpoints: Advanced Debugging with XCodeBeyond Breakpoints: Advanced Debugging with XCode
Beyond Breakpoints: Advanced Debugging with XCode
 
Active Record Query Interface (1), Season 2
Active Record Query Interface (1), Season 2Active Record Query Interface (1), Season 2
Active Record Query Interface (1), Season 2
 
The Ring programming language version 1.2 book - Part 32 of 84
The Ring programming language version 1.2 book - Part 32 of 84The Ring programming language version 1.2 book - Part 32 of 84
The Ring programming language version 1.2 book - Part 32 of 84
 
Optimizing Queries with Explain
Optimizing Queries with ExplainOptimizing Queries with Explain
Optimizing Queries with Explain
 
The Ring programming language version 1.8 book - Part 34 of 202
The Ring programming language version 1.8 book - Part 34 of 202The Ring programming language version 1.8 book - Part 34 of 202
The Ring programming language version 1.8 book - Part 34 of 202
 
The Ring programming language version 1.6 book - Part 31 of 189
The Ring programming language version 1.6 book - Part 31 of 189The Ring programming language version 1.6 book - Part 31 of 189
The Ring programming language version 1.6 book - Part 31 of 189
 
Explain
ExplainExplain
Explain
 
The Ring programming language version 1.5.1 book - Part 27 of 180
The Ring programming language version 1.5.1 book - Part 27 of 180The Ring programming language version 1.5.1 book - Part 27 of 180
The Ring programming language version 1.5.1 book - Part 27 of 180
 
Scaling MySQL Strategies for Developers
Scaling MySQL Strategies for DevelopersScaling MySQL Strategies for Developers
Scaling MySQL Strategies for Developers
 
The Ring programming language version 1.4.1 book - Part 8 of 31
The Ring programming language version 1.4.1 book - Part 8 of 31The Ring programming language version 1.4.1 book - Part 8 of 31
The Ring programming language version 1.4.1 book - Part 8 of 31
 
The Ring programming language version 1.7 book - Part 32 of 196
The Ring programming language version 1.7 book - Part 32 of 196The Ring programming language version 1.7 book - Part 32 of 196
The Ring programming language version 1.7 book - Part 32 of 196
 
ActiveRecord Query Interface (2), Season 2
ActiveRecord Query Interface (2), Season 2ActiveRecord Query Interface (2), Season 2
ActiveRecord Query Interface (2), Season 2
 
Introduction to SQLAlchemy ORM
Introduction to SQLAlchemy ORMIntroduction to SQLAlchemy ORM
Introduction to SQLAlchemy ORM
 
Rapid and Scalable Development with MongoDB, PyMongo, and Ming
Rapid and Scalable Development with MongoDB, PyMongo, and MingRapid and Scalable Development with MongoDB, PyMongo, and Ming
Rapid and Scalable Development with MongoDB, PyMongo, and Ming
 
Database security
Database securityDatabase security
Database security
 
The Ring programming language version 1.4.1 book - Part 13 of 31
The Ring programming language version 1.4.1 book - Part 13 of 31The Ring programming language version 1.4.1 book - Part 13 of 31
The Ring programming language version 1.4.1 book - Part 13 of 31
 
32373 uploading-php-shell-through-sql-injection
32373 uploading-php-shell-through-sql-injection32373 uploading-php-shell-through-sql-injection
32373 uploading-php-shell-through-sql-injection
 
Maven + Jsf + Richfaces + Jxl + Jdbc - Complete Code Example
Maven + Jsf + Richfaces + Jxl + Jdbc - Complete Code ExampleMaven + Jsf + Richfaces + Jxl + Jdbc - Complete Code Example
Maven + Jsf + Richfaces + Jxl + Jdbc - Complete Code Example
 
Into Clojure
Into ClojureInto Clojure
Into Clojure
 
MySQL constraints
MySQL constraintsMySQL constraints
MySQL constraints
 

Similar to Rails Concurrency Gotchas

Timothy N. Tsvetkov, Rails 3.1
Timothy N. Tsvetkov, Rails 3.1Timothy N. Tsvetkov, Rails 3.1
Timothy N. Tsvetkov, Rails 3.1Evil Martians
 
Replication Troubleshooting in Classic VS GTID
Replication Troubleshooting in Classic VS GTIDReplication Troubleshooting in Classic VS GTID
Replication Troubleshooting in Classic VS GTIDMydbops
 
PyCon 2010 SQLAlchemy tutorial
PyCon 2010 SQLAlchemy tutorialPyCon 2010 SQLAlchemy tutorial
PyCon 2010 SQLAlchemy tutorialjbellis
 
OpenWorld 2018 - Common Application Developer Disasters
OpenWorld 2018 - Common Application Developer DisastersOpenWorld 2018 - Common Application Developer Disasters
OpenWorld 2018 - Common Application Developer DisastersConnor McDonald
 
Ruxmon cve 2012-2661
Ruxmon cve 2012-2661Ruxmon cve 2012-2661
Ruxmon cve 2012-2661snyff
 
Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007Rabble .
 
Ruby On Rails Pitfalls
Ruby On Rails PitfallsRuby On Rails Pitfalls
Ruby On Rails PitfallsRobin Lu
 
Kickin' Ass with Cache-Fu (without notes)
Kickin' Ass with Cache-Fu (without notes)Kickin' Ass with Cache-Fu (without notes)
Kickin' Ass with Cache-Fu (without notes)err
 
Grow your own tools - VilniusRB
Grow your own tools - VilniusRBGrow your own tools - VilniusRB
Grow your own tools - VilniusRBRemigijus Jodelis
 
How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...
How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...
How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...Docker, Inc.
 
Beyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeBeyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeWim Godden
 
Beyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeBeyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeWim Godden
 
Using Perl Stored Procedures for MariaDB
Using Perl Stored Procedures for MariaDBUsing Perl Stored Procedures for MariaDB
Using Perl Stored Procedures for MariaDBAntony T Curtis
 
PHP security audits
PHP security auditsPHP security audits
PHP security auditsDamien Seguy
 
Beyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeBeyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeWim Godden
 
Ruby on rails
Ruby on rails Ruby on rails
Ruby on rails Mohit Jain
 
Easy MySQL Replication Setup and Troubleshooting
Easy MySQL Replication Setup and TroubleshootingEasy MySQL Replication Setup and Troubleshooting
Easy MySQL Replication Setup and TroubleshootingBob Burgess
 

Similar to Rails Concurrency Gotchas (20)

Timothy N. Tsvetkov, Rails 3.1
Timothy N. Tsvetkov, Rails 3.1Timothy N. Tsvetkov, Rails 3.1
Timothy N. Tsvetkov, Rails 3.1
 
Deadlocks in MySQL
Deadlocks in MySQLDeadlocks in MySQL
Deadlocks in MySQL
 
Replication Troubleshooting in Classic VS GTID
Replication Troubleshooting in Classic VS GTIDReplication Troubleshooting in Classic VS GTID
Replication Troubleshooting in Classic VS GTID
 
PyCon 2010 SQLAlchemy tutorial
PyCon 2010 SQLAlchemy tutorialPyCon 2010 SQLAlchemy tutorial
PyCon 2010 SQLAlchemy tutorial
 
OpenWorld 2018 - Common Application Developer Disasters
OpenWorld 2018 - Common Application Developer DisastersOpenWorld 2018 - Common Application Developer Disasters
OpenWorld 2018 - Common Application Developer Disasters
 
Ruxmon cve 2012-2661
Ruxmon cve 2012-2661Ruxmon cve 2012-2661
Ruxmon cve 2012-2661
 
Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007Introduction to Active Record - Silicon Valley Ruby Conference 2007
Introduction to Active Record - Silicon Valley Ruby Conference 2007
 
Ruby On Rails Pitfalls
Ruby On Rails PitfallsRuby On Rails Pitfalls
Ruby On Rails Pitfalls
 
Kickin' Ass with Cache-Fu (without notes)
Kickin' Ass with Cache-Fu (without notes)Kickin' Ass with Cache-Fu (without notes)
Kickin' Ass with Cache-Fu (without notes)
 
Grow your own tools - VilniusRB
Grow your own tools - VilniusRBGrow your own tools - VilniusRB
Grow your own tools - VilniusRB
 
How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...
How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...
How and Why Prometheus' New Storage Engine Pushes the Limits of Time Series D...
 
Beyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeBeyond php - it's not (just) about the code
Beyond php - it's not (just) about the code
 
Instalar MySQL CentOS
Instalar MySQL CentOSInstalar MySQL CentOS
Instalar MySQL CentOS
 
Beyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeBeyond php - it's not (just) about the code
Beyond php - it's not (just) about the code
 
Using Perl Stored Procedures for MariaDB
Using Perl Stored Procedures for MariaDBUsing Perl Stored Procedures for MariaDB
Using Perl Stored Procedures for MariaDB
 
Why ruby
Why rubyWhy ruby
Why ruby
 
PHP security audits
PHP security auditsPHP security audits
PHP security audits
 
Beyond php - it's not (just) about the code
Beyond php - it's not (just) about the codeBeyond php - it's not (just) about the code
Beyond php - it's not (just) about the code
 
Ruby on rails
Ruby on rails Ruby on rails
Ruby on rails
 
Easy MySQL Replication Setup and Troubleshooting
Easy MySQL Replication Setup and TroubleshootingEasy MySQL Replication Setup and Troubleshooting
Easy MySQL Replication Setup and Troubleshooting
 

Recently uploaded

Automating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps ScriptAutomating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps Scriptwesley chun
 
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProduct Anonymous
 
Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfsudhanshuwaghmare1
 
Top 10 Most Downloaded Games on Play Store in 2024
Top 10 Most Downloaded Games on Play Store in 2024Top 10 Most Downloaded Games on Play Store in 2024
Top 10 Most Downloaded Games on Play Store in 2024SynarionITSolutions
 
Manulife - Insurer Innovation Award 2024
Manulife - Insurer Innovation Award 2024Manulife - Insurer Innovation Award 2024
Manulife - Insurer Innovation Award 2024The Digital Insurer
 
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost SavingRepurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost SavingEdi Saputra
 
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, AdobeApidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobeapidays
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...Martijn de Jong
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherRemote DBA Services
 
Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businesspanagenda
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationRadu Cotescu
 
Exploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone ProcessorsExploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone Processorsdebabhi2
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024The Digital Insurer
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...DianaGray10
 
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdfUnderstanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdfUK Journal
 
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...Miguel Araújo
 
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024The Digital Insurer
 
Tata AIG General Insurance Company - Insurer Innovation Award 2024
Tata AIG General Insurance Company - Insurer Innovation Award 2024Tata AIG General Insurance Company - Insurer Innovation Award 2024
Tata AIG General Insurance Company - Insurer Innovation Award 2024The Digital Insurer
 
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...Neo4j
 
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...apidays
 

Recently uploaded (20)

Automating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps ScriptAutomating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps Script
 
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
 
Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdf
 
Top 10 Most Downloaded Games on Play Store in 2024
Top 10 Most Downloaded Games on Play Store in 2024Top 10 Most Downloaded Games on Play Store in 2024
Top 10 Most Downloaded Games on Play Store in 2024
 
Manulife - Insurer Innovation Award 2024
Manulife - Insurer Innovation Award 2024Manulife - Insurer Innovation Award 2024
Manulife - Insurer Innovation Award 2024
 
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost SavingRepurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
Repurposing LNG terminals for Hydrogen Ammonia: Feasibility and Cost Saving
 
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, AdobeApidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a Fresher
 
Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire business
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organization
 
Exploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone ProcessorsExploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone Processors
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
 
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdfUnderstanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
 
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
 
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
Bajaj Allianz Life Insurance Company - Insurer Innovation Award 2024
 
Tata AIG General Insurance Company - Insurer Innovation Award 2024
Tata AIG General Insurance Company - Insurer Innovation Award 2024Tata AIG General Insurance Company - Insurer Innovation Award 2024
Tata AIG General Insurance Company - Insurer Innovation Award 2024
 
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
 
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
 

Rails Concurrency Gotchas

  • 1. Surpresas no uso de Concorrência em by Marcos Toledo Rails
  • 7. “I’m trying to unsubscribe but it says email has already been taken”!
  • 8. validates_uniquen ess_of # id :integer(4) # email :string(255) class User < ActiveRecord::Base validates_uniqueness_of :email end
  • 9. WTF .. ?
  • 11. Why?
  • 14. Uniqueness Constraint! class AddUniquenessIndexToUserEmail < ActiveRecord::Migration def self.up add_index :users, :email, :unique => true end def self.down remove_index :users, :email end end
  • 17.
  • 19. NoMethodError: undefined method `title' for nil:NilClass “nil post”?
  • 20. :dependent => :destroy class Comment < ActiveRecord::Base belongs_to :post validates_presence_of :post end class Post < ActiveRecord::Base has_many :comments, :dependent => :destroy end
  • 21. WTF .. ?
  • 23. Why?
  • 26. Database Foreign Keys! 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
  • 28. 1 - Noob “Use database constraints”
  • 35. Infinite Loop 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
  • 36. WTF .. ?
  • 37. Transaction Isolation Level “I” in ACID
  • 38. 1 - READ UNCOMMITTED 2 - READ COMMITTED 3 - REPEATABLE READ
  • 39. Postgre READ-COMMITTED MySQL REPEATABLE-READ
  • 40. 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)
  • 41. 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)
  • 42. 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)
  • 43. 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)
  • 45. READ- COMMITTED! /etc/my.cnf [mysqld] transaction-isolation = READ- COMMITTED Just like postgre ;)
  • 46. 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)
  • 48. 2 - Beginner “Use READ- COMMITTED”
  • 50. Production Console > 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}
  • 51. WTF .. ?
  • 52. # login :string(255) # options :text class User < ActiveRecord::Base serialize :options def activate self.options = {:active => true} self.save end end
  • 53. No ‘:active => false’!
  • 54. Why?
  • 55. Dirty Checking > 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}
  • 57. // u1.update_attributes :options => {:active => true} UPDATE `users` SET `options` = '--- n:active: truen' WHERE (`users`.`id` = 1) // u2.update_attributes :login => "mtoledo" UPDATE `users` SET `login` = 'mtoledo', `options` = '--- n:active: falsen' WHERE (`users`.`id` = 1)
  • 60. # id :integer(4) # user_id :integer(4) # lock_version :integer(4) # options :text class UserOptions < ActiveRecord::Base serialize :options belongs_to :user end
  • 62. 3 - Half Mouth “Use an Options Model”
  • 64. production.log ERROR 1213 (40001): Deadlock found when trying to get lock;try restarting transaction
  • 66. 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
  • 67. WTF .. ?
  • 68. Transac Transaction 1 SELECT * FOR tion 2 UPDATE FROM `comments` WHERE `id` = 1 SELECT * FOR UPDATE locked.. FROM `posts` WHERE `id` = 1 SELECT * FOR UPDATE FROM `posts` DEADLOCK! WHERE `id` = 1 SELECT * FOR UPDATE FROM `comments` WHERE `id` = 1
  • 69. Why?
  • 70. Transactions lock on update
  • 71. Transac Transaction 1 UPDATE tion 2 `comments` SET `title` = “Foo” WHERE `id` = 1 UPDATE `posts` SET `title` = “bar” locked.. WHERE `id` = 1 UPDATE `posts` SET `title` = “bar” WHERE `id` = 1 DEADLOCK! UPDATE `comments` SET `title` = “foo” WHERE `id` = 1
  • 75. 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
  • 77. Avoids updating all comments class Comment < ActiveRecord::Base def visible? post.published? && self.published? end end
  • 79. 4 - Advanced “Acquire locks consistently”
  • 81. “There is no safe way to use Thread#kill or Thread#raise.” http://blog.headius.com/2008/02/rubys-threadraise-threadkill-timeoutrb.html
  • 82. Killing ruby (MRI) threads - bluepill - god - monit - you and me
  • 83. Simple Transaction class Order belongs_to :user after_create :mark_user_as_customer def mark_user_as_customer user.customer = true user.save end end
  • 84. 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>
  • 85. WTF .. ?
  • 86. If you kill your Ruby (MRI) Threads ..
  • 87. ActiveRecord will partially commit your transactions!
  • 88. Why?
  • 89. ActiveRecord transaction (version for kids) def transaction yield rescue Exception => e rollback ensure commit unless rolledback? end
  • 90. 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
  • 91. 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
  • 92. It commits! def transaction yield # thread killed inside yield rescue Exception => e rollback # nothing is raised ensure commit unless rolledback? end
  • 94. DON’T Kill Your Threads! Headius Approv
  • 95. If you do, don’t use ActiveRecord + Threads + MRI
  • 96. 5 Record Certified Enterprise Active - ARCECA “Don’t use Concurrency Architect Thread#kill/raise ”
  • 99. 1 - Noob “Use database constraints”
  • 100. 2 - Beginner “Use READ- COMMITTED”
  • 101. 3 - Half Mouth “Use an Options Model”
  • 102. 4 - Advanced “Acquire locks consistently”
  • 103. 5 Record Certified Enterprise Active - ARCECA “Don’t use Concurrency Architect Thread#kill/raise ”

Editor's Notes

  1. \n
  2. \n
  3. \n
  4. \n
  5. \n
  6. \n
  7. \n
  8. \n
  9. \n
  10. \n
  11. \n
  12. \n
  13. \n
  14. \n
  15. \n
  16. \n
  17. \n
  18. \n
  19. \n
  20. \n
  21. \n
  22. \n
  23. \n
  24. \n
  25. \n
  26. \n
  27. \n
  28. \n
  29. \n
  30. \n
  31. \n
  32. \n
  33. \n
  34. \n
  35. \n
  36. \n
  37. \n
  38. \n
  39. \n
  40. \n
  41. \n
  42. \n
  43. \n
  44. \n
  45. \n
  46. \n
  47. \n
  48. \n
  49. \n
  50. \n
  51. \n
  52. \n
  53. \n
  54. \n
  55. \n
  56. \n
  57. \n
  58. \n
  59. \n
  60. \n
  61. \n
  62. \n
  63. \n
  64. \n
  65. \n
  66. \n
  67. \n
  68. \n
  69. \n
  70. \n
  71. \n
  72. \n
  73. \n
  74. \n
  75. \n
  76. \n
  77. \n
  78. \n
  79. \n
  80. \n
  81. \n
  82. \n
  83. \n
  84. \n
  85. \n
  86. \n
  87. \n
  88. \n
  89. \n
  90. \n
  91. \n
  92. \n
  93. \n
  94. \n
  95. \n
  96. \n
  97. \n
  98. \n
  99. \n
  100. \n
  101. \n
  102. \n
  103. \n
  104. \n
  105. \n