Rails: Shadow Facets of Concurrency

Eugene Kalenkovich

@UncleGene






audience.except(&:can_see_this).each do |person|
  # Please!
  person.move(:closer)
end
          

An Ode to 6+ Concurrency Facets (~10 minutes each) in ??? Minutes

You may not worry about concurrency if:

  • You use Rails
  • Your application is single-threaded
  • You have a very low traffic

Do you worry?

When?

RAILS_ENV=development

RAILS_ENV=production

  • Shared resources
  • Unpredictable request processing
    • Who
    • When

Part 1

Rails and Concurrency

RAILS_ENV=test

database.yml:

test:
  adapter: postgresql  # mysql2
  database: your_db_test
            
test_helper.rb (spec_helper.rb):

def concurrently processes = 10
  processes.times do
    fork do
      yield
    end
  end
  assert Process.waitall.map(&:last).all? &:success?
end
            

def concurrently processes = 10
  processes.times do
    fork do
      yield
    end
  end
  assert Process.waitall.map(&:last).all? &:success?
end
            

def concurrently processes = 10
  processes.times do
    fork do
      yield
    end
  end
  assert Process.waitall.map(&:last).all? &:success?
end
            

RAILS_ENV=test


def concurrently processes = 10
  ActiveRecord::Base.remove_connection
  processes.times do
    fork do
      begin
        ActiveRecord::Base.establish_connection
        yield
      rescue => e
        puts "#{e.class.name}: #{e}" && exit 1
      ensure
        ActiveRecord::Base.remove_connection
      end
    end
  end
  ActiveRecord::Base.establish_connection
  assert Process.waitall.map(&:last).all? &:success?
end  
          

def concurrently processes = 10
  ActiveRecord::Base.remove_connection
  processes.times do
    fork do
      begin
        ActiveRecord::Base.establish_connection
        yield
      rescue => e
        puts "#{e.class.name}: #{e}" && exit 1
      ensure
        ActiveRecord::Base.remove_connection
      end
    end
  end
  ActiveRecord::Base.establish_connection
  assert Process.waitall.map(&:last).all? &:success?
end
          

def concurrently processes = 10
  ActiveRecord::Base.remove_connection
  processes.times do
    fork do
      begin
        ActiveRecord::Base.establish_connection
        yield
      rescue => e
        puts "#{e.class.name}: #{e}" && exit 1
      ensure
        ActiveRecord::Base.remove_connection
      end
    end
  end
  ActiveRecord::Base.establish_connection
  assert Process.waitall.map(&:last).all? &:success?
end
          

Facet #1: One and Only

first_or_create


class Number < ActiveRecord::Base
  # t.integer :value
end   
          

describe Number do
  it 'should have unique values' do
    concurrently do
      10.times do
        Number.where(value: Number.count).first_or_create
      end
    end
    Number.count.must_equal Number.select('distinct value').count
  end
end
          

describe Number do
  it 'should have unique values' do
    concurrently do
      10.times do
        Number.where(value: Number.count).first_or_create
      end
    end
    Number.count.must_equal Number.select('distinct value').count
  end
end
          

describe Number do
  it 'should have unique values' do
    concurrently do
      10.times do
        Number.where(value: Number.count).first_or_create
      end
    end
    Number.count.must_equal Number.select('distinct value').count
  end
end
          

1) Failure:
test_0001_should_have_unique_values(Number) :
Expected: 49
  Actual: 99
          

first_or_create.inspect


> Number.where(value: 3).first_or_create
  Number Load (16.2ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.4ms)  COMMIT
            

> Number.where(value: 3).first_or_create
  Number Load (16.2ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.4ms)  COMMIT
            

> Number.where(value: 3).first_or_create
  Number Load (16.2ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.4ms)  COMMIT
            

> Number.where(value: 3).first_or_create
  Number Load (0.4ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
            

> Number.where(value: 3).first_or_create
  Number Load (0.4ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
            

> Number.where(value: 3).first_or_create
  Number Load (16.2ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.4ms)  COMMIT
            

> Number.where(value: 3).first_or_create
  Number Load (16.2ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.4ms)  COMMIT
            

> Number.where(value: 3).first_or_create
  Number Load (16.2ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (9.0ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.4ms)  COMMIT
            

> Number.where(value: 3).first_or_create
  Number Load (0.4ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
            

> Number.where(value: 3).first_or_create
  Number Load (0.4ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
            

> Number.where(value: 3).first_or_create
  Number Load (0.4ms)  SELECT "numbers".* FROM "numbers" WHERE "numbers"."value" = 3 LIMIT 1
   (0.2ms)  BEGIN
  SQL (0.3ms)  INSERT INTO "numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 3]]
   (0.5ms)  COMMIT
            

validates :uniquiness


class ValidatedNumber < ActiveRecord::Base
  validates :value, uniqueness: true
end
            

describe ValidatedNumber do
  it 'should have unique values' do
    concurrently do
      10.times do
        ValidatedNumber.create{|r| r.value = ValidatedNumber.count}
      end
    end
    unique = ValidatedNumber.select("distinct value").count
    ValidatedNumber.count.must_equal unique
  end
end
            

1) Failure:
test_0001_should_have_unique_values(ValidatedNumber):
Expected: 37
  Actual: 96

            

> ValidatedNumber.create(value: 1)
   (0.1ms)  BEGIN
  ValidatedNumber Exists (25.2ms)  SELECT 1 AS one FROM "validated_numbers" WHERE "validated_numbers"."value" = 1 LIMIT 1
  SQL (14.7ms)  INSERT INTO "validated_numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 1]]
   (0.7ms)  COMMIT
            

> ValidatedNumber.create(value: 1)
   (0.1ms)  BEGIN
  ValidatedNumber Exists (25.2ms)  SELECT 1 AS one FROM "validated_numbers" WHERE "validated_numbers"."value" = 1 LIMIT 1
  SQL (14.7ms)  INSERT INTO "validated_numbers" ("value") VALUES ($1) RETURNING "id"  [["value", 1]]
   (0.7ms)  COMMIT
          

unique.fix :db


class CreateConstrainedNumbers < ActiveRecord::Migration
  def change
    create_table :constrained_numbers do |t|
      t.integer :value
    end
    add_index :constrained_numbers, :value, unique: true
  end
end
          

class CreateConstrainedNumbers < ActiveRecord::Migration
  def change
    create_table :constrained_numbers do |t|
      t.integer :value
    end
    add_index :constrained_numbers, :value, unique: true
  end
end
          

# Running tests:

ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
[...]
          

# Running tests:

ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
ActiveRecord::RecordNotUnique: PG::Error: ERROR:  duplicate key value violates unique constraint "index_constrained_numbers_on_value"
DETAIL:  Key (value)=(0) already exists.
: INSERT INTO "constrained_numbers" ("value") VALUES ($1) RETURNING "id"
[...]
          

unique.fix :db, diy: true


class SafeNumber < ActiveRecord::Base
  def self.first_or_create_where(*args)
    where(*args).first_or_create
  rescue ActiveRecord::RecordNotUnique
    retry
  end
end
          

class SafeNumber < ActiveRecord::Base
  def self.first_or_create_where(*args)
    where(*args).first_or_create
  rescue ActiveRecord::RecordNotUnique
    retry
  end
end
          

# Running tests:

.

Finished tests in 8.337332s, 0.1199 tests/s, 0.2399 assertions/s.

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
          

ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: INSERT INTO `safe_numbers` (`value`) VALUES (132)
ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: INSERT INTO `safe_numbers` (`value`) VALUES (132)
F
          

ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: INSERT INTO `safe_numbers` (`value`) VALUES (132)
ActiveRecord::StatementInvalid: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: INSERT INTO `safe_numbers` (`value`) VALUES (132)
F
          

unique.fix :db, diy: true, mysql: true


def self.first_or_create_where(*args)
  where(*args).first_or_create
rescue ActiveRecord::RecordNotUnique
  retry
rescue ActiveRecord::StatementInvalid => e
  retry if e.message =~ /Deadlock/
  # Thank you, MySQL and Mysql2!
  raise
end
          

def self.first_or_create_where(*args)
  where(*args).first_or_create
rescue ActiveRecord::RecordNotUnique
  retry
rescue ActiveRecord::StatementInvalid => e
  retry if e.message =~ /Deadlock/
  # Thank you, MySQL and Mysql2!
  raise
end
          

# Running tests:

.

Finished tests in 3.234423s, 0.3092 tests/s, 0.6183 assertions/s.

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
          

Facet #2:
I Can Haz One or Many?

or Guilty by Association

class Dog


class Dog < ActiveRecord::Base
  has_one :head
  has_many :legs
end
          

class Head < ActiveRecord::Base
  belongs_to :dog
end

class Leg < ActiveRecord::Base
  belongs_to :dog
end
          

Dog.build


20.times{ Dog.create }

concurrently do
  begin
    headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
    headless && headless.create_head

    legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
    legless && legless.legs = 4.times.map{ Leg.create }
  end while headless || legless
end
          

20.times{ Dog.create }

concurrently do
  begin
    headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
    headless && headless.create_head

    legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
    legless && legless.legs = 4.times.map{ Leg.create }
  end while headless || legless
end
          

20.times{ Dog.create }

concurrently do
  begin
    headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
    headless && headless.create_head

    legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
    legless && legless.legs = 4.times.map{ Leg.create }
  end while headless || legless
end
          

20.times{ Dog.create }

concurrently do
  begin
    headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
    headless && headless.create_head

    legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
    legless && legless.legs = 4.times.map{ Leg.create }
  end while headless || legless
end
          

assert_sanity


Dog.all_str.must_equal "20 dogs with 1 head and 4 legs"
          

--- expected
+++ actual
@@ -1 +1 @@
-"20 dogs with 1 head and 4 legs"
+"3 dogs with 1 head and 8 legs, 2 dogs with 2 heads and 28 legs, 2 dogs with 1 head and 12 legs, 1 dog with 1 head and 4 legs, 1 dog with 4 heads and 24 legs, 1 dog with 4 heads and 32 legs, 1 dog with 3 heads and 16 legs, 1 dog with 2 heads and 32 legs, 1 dog with 1 head and 16 legs, 1 dog with 6 heads and 8 legs, 1 dog with 1 head and 20 legs, 1 dog with 2 heads and 16 legs, 1 dog with 4 heads and 36 legs, 1 dog with 5 heads and 24 legs, 1 dog with 8 heads and 20 legs, 1 dog with 2 heads and 40 legs"
          

--- expected
+++ actual
@@ -1 +1 @@
-"20 dogs with 1 head and 4 legs"
+"3 dogs with 1 head and 8 legs, 2 dogs with 2 heads and 28 legs, 2 dogs with 1 head and 12 legs, 1 dog with 1 head and 4 legs, 1 dog with 4 heads and 24 legs, 1 dog with 4 heads and 32 legs, 1 dog with 3 heads and 16 legs, 1 dog with 2 heads and 32 legs, 1 dog with 1 head and 16 legs, 1 dog with 6 heads and 8 legs, 1 dog with 1 head and 20 legs, 1 dog with 2 heads and 16 legs, 1 dog with 4 heads and 36 legs, 1 dog with 5 heads and 24 legs, 1 dog with 8 heads and 20 legs, 1 dog with 2 heads and 40 legs"
          

dog.head.inspect


> Dog.first.create_head
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (12.0ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", 1]]
   (0.9ms)  COMMIT
  Head Load (0.2ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 1
   (0.4ms)  COMMIT
          

> Dog.first.create_head
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (12.0ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", 1]]
   (0.9ms)  COMMIT
  Head Load (0.2ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 1
   (0.4ms)  COMMIT
          

> Dog.first.create_head
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (12.0ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", 1]]
   (0.9ms)  COMMIT
  Head Load (0.2ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 1
   (0.4ms)  COMMIT
          

> Dog.first.create_head
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (12.0ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", 1]]
   (0.9ms)  COMMIT
  Head Load (0.2ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 1
   (0.4ms)  COMMIT
          

> Dog.first.create_head
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (12.0ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", 1]]
   (0.9ms)  COMMIT
  Head Load (0.2ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 1
   (0.4ms)  COMMIT
          

dog.legs.inspect


>   Dog.first.legs = Leg.all.sample(4)
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
  Leg Load (0.2ms)  SELECT "legs".* FROM "legs"
  Leg Load (0.3ms)  SELECT "legs".* FROM "legs" WHERE "legs"."dog_id" = 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 6
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 3
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 10
   (0.1ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 5
   (0.5ms)  COMMIT
          

> Dog.first.legs = Leg.all.sample(4)
  Dog Load (0.4ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
  Leg Load (0.3ms)  SELECT "legs".* FROM "legs"
  Leg Load (0.3ms)  SELECT "legs".* FROM "legs" WHERE "legs"."dog_id" = 1
   (0.1ms)  BEGIN
  SQL (0.3ms)  UPDATE "legs" SET "dog_id" = NULL WHERE "legs"."dog_id" = 1 AND "legs"."id" IN (10, 5)
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 2
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 9
   (0.6ms)  COMMIT
          

> Dog.first.legs = Leg.all.sample(4)
  Dog Load (0.4ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
  Leg Load (0.3ms)  SELECT "legs".* FROM "legs"
  Leg Load (0.3ms)  SELECT "legs".* FROM "legs" WHERE "legs"."dog_id" = 1
   (0.1ms)  BEGIN
  SQL (0.3ms)  UPDATE "legs" SET "dog_id" = NULL WHERE "legs"."dog_id" = 1 AND "legs"."id" IN (10, 5)
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 2
   (0.2ms)  UPDATE "legs" SET "dog_id" = 1 WHERE "legs"."id" = 9
   (0.6ms)  COMMIT
          

dog.head.fix

unique.fix :db, diy: true, mysql: mysql?

dog.fix :db


headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
headless && Dog.transaction do
  headless.lock!
  headless.create_head
end
legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
legless && Dog.transaction do
  legless.lock!
  legless.legs = 4.times.map{ Leg.create }
end
          

headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
headless && Dog.transaction do
  headless.lock!
  headless.create_head
end
legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
legless && Dog.transaction do
  legless.lock!
  legless.legs = 4.times.map{ Leg.create }
end
          

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
          

--- expected
+++ actual
@@ -1 +1 @@
-"50 dogs with 1 head and 4 legs"
+"30 dogs with 1 head and 4 legs, 11 dogs with 2 heads and 4 legs, 4 dogs with 3 heads and 4 legs, 4 dogs with 4 heads and 4 legs, 1 dog with 5 heads and 4 legs"
          

dog.refix :db


> Dog.first.create_head
  Dog Load (0.3ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (12.0ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", 1]]
   (0.9ms)  COMMIT
  Head Load (0.2ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.2ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 1
   (0.4ms)  COMMIT
          

> Dog.first.head = Head.create
  Dog Load (39.8ms)  SELECT "dogs".* FROM "dogs" LIMIT 1
   (0.1ms)  BEGIN
  SQL (49.1ms)  INSERT INTO "heads" ("dog_id") VALUES ($1) RETURNING "id"  [["dog_id", nil]]
   (0.6ms)  COMMIT
"replace"
  Head Load (0.4ms)  SELECT "heads".* FROM "heads" WHERE "heads"."dog_id" = 1 LIMIT 1
   (0.1ms)  BEGIN
   (0.4ms)  UPDATE "heads" SET "dog_id" = NULL WHERE "heads"."id" = 2
   (0.2ms)  UPDATE "heads" SET "dog_id" = 1 WHERE "heads"."id" = 3
   (0.5ms)  COMMIT
          

headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
headless && Dog.transaction do
  headless.lock!
  headless.head = Head.create
end
legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
legless && Dog.transaction do
  legless.lock!
  legless.legs = 4.times.map{ Leg.create }
end
          

# Running tests:

.

Finished tests in 2.527906s, 0.3956 tests/s, 0.7912 assertions/s.

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
          

dog.fix :diy


headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
if headless
  headless.create_head
  Head.update_all({ dog_id: nil },
    { id: Head.where(dog_id: headless.id).order(:id).pluck(:id)[0..-2] })
end
legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
if legless
  legless.legs = 4.times.map{ Leg.create }
  Leg.update_all({ dog_id: nil },
    { id: Leg.where(dog_id: legless.id).order(:id).pluck(:id)[0..-5] })
end
          

headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
if headless
  headless.create_head
  Head.update_all({ dog_id: nil },
    { id: Head.where(dog_id: headless.id).order(:id).pluck(:id)[0..-2] })
end
legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
if legless
  legless.legs = 4.times.map{ Leg.create }
  Leg.update_all({ dog_id: nil },
    { id: Leg.where(dog_id: legless.id).order(:id).pluck(:id)[0..-5] })
end
          

headless = Dog.includes(:head).where(heads: {dog_id: nil}).first
if headless
  headless.create_head
  Head.update_all({ dog_id: nil },
    { id: Head.where(dog_id: headless.id).order(:id).pluck(:id)[0..-2] })
end
legless = Dog.includes(:legs).where(legs: {dog_id: nil}).first
if legless
  legless.legs = 4.times.map{ Leg.create }
  Leg.update_all({ dog_id: nil },
    { id: Leg.where(dog_id: legless.id).order(:id).pluck(:id)[0..-5] })
end
          

# Running tests:

.

Finished tests in 2.957711s, 0.3956 tests/s, 0.7912 assertions/s.

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips
          

Facet #3: Emigration

db:migrate

  • Add table - OK
  • Add column, add/remove index - so-so
  • Remove column - oops (before 4.0)

try :remove_column


remove_column :extra_columns, :extra
          

> ExtraColumn.create
   (0.2ms)  BEGIN
  SQL (34.9ms)  INSERT INTO "extra_columns" ("extra", "value") VALUES ($1, $2) RETURNING "id"  [["extra", nil], ["value", nil]]
   (0.2ms)  ROLLBACK
ActiveRecord::StatementInvalid: PG::Error: ERROR:  column "extra" of relation "extra_columns" does not exist at character 30
: INSERT INTO "extra_columns" ("extra", "value") VALUES ($1, $2) RETURNING "id"
          

> ExtraColumn.create
   (0.2ms)  BEGIN
  SQL (34.9ms)  INSERT INTO "extra_columns" ("extra", "value") VALUES ($1, $2) RETURNING "id"  [["extra", nil], ["value", nil]]
   (0.2ms)  ROLLBACK
ActiveRecord::StatementInvalid: PG::Error: ERROR:  column "extra" of relation "extra_columns" does not exist at character 30
: INSERT INTO "extra_columns" ("extra", "value") VALUES ($1, $2) RETURNING "id"
          

remove_column.inspect


def columns
  @columns ||= connection.schema_cache.columns[table_name].map do |col|
    col = col.dup
    col.primary = (col.name == primary_key)
    col
  end
end
          
  • All operations are defined in terms of klass.columns (directly or indirectly)
  • True for Rails 2.x, 3.0 3.2

remove_column.fix

    
    class ExtraColumn < ActiveRecord::Base
      def self.columns
        @my_columns ||= super.reject{ |c|
          c.name == 'extra'
        }
      end
    end
                
  • Deploy
  • 
    remove_column :extra_columns, :extra
                
  • Do cleanup and re-deploy            

remove_column.fix :rails_3_1

Facet #4: Assets & Liabilities

deploy :shutdown

deploy :rolling_swap

deploy :rolling_shutdown

rake assets:fix


config.action_controller.asset_host = "//your.super.asset.host"
          
  • For S3 you can use gem 'asset_sync'
  • ... or roll out your own solution to upload precompiled assets before deployment
  • Do not delete old assets too early

config.assets.digest = true
          

Part 2

Off Rails

Facet #5. Development and Deployment

Concurrency of development with deployment . . . has almost always proven counterproductive

Harold Brown

Development and Deployment

I Can Haz Four Volunteers?

changed_feature.deploy!

  1. Teach old dog new tricks (but do not show them)
  2. Teach new dog old tricks
  3. Swap your dogs
  4. Wait...
  5. Now you can forget old tricks
  • Your website is an API server and a client at the same time (but client is behind!)
  • Treat changing features as a new API version

You may not worry about concurrency if:

  • You do not care
  • But if you do – worry, and you will (almost) always find your way
Rails is omakase. A team of chefs picked out the ingredients, designed the APIs, and arranged the order of consumption on your behalf according to their idea of what would make for a tasty full-stack framework.

DHH

NOTICE: Consuming raw or undercooked meats, poultry, seafood, shellfish, or eggs may increase your risk of foodborne illness

Facets #6 - #N. Homework

Design a Presentation Web App

  • User can add a slide at an arbitrary place
  • User can easily reorder slides
  • All edits are saved "behind the scenes"


Consider:

  • Synchronous vs. asynchronous messaging
  • Lost slide reordering messages
  • Reordering messages coming out-of-order
  • User adding, editing and/or reordering on a stale page
  • Extra bonus: collaborative editing


Please talk to me when you are done...

Credits

  • Hakim El Hattab - Reveal.js, http://lab.hakim.se/reveal-js/#/
  • "Concurrency." Merriam-Webster.com. Merriam-Webster, n.d. Web. 23 Aug. 2013. [http://www.merriam-webster.com/dictionary/concurrency]
  • Wikipedia - http://en.wikipedia.org/wiki/Concurrency_(computer_science)
  • David Heinemeier Hansson - http://david.heinemeierhansson.com/2012/rails-is-omakase.html
  • Harold Brown - http://www.foreignaffairs.com/articles/40540/harold-brown/is-sdi-technically-feasible
  • Images:
    • By Garrett Fitzgerald [CC-BY-SA-2.5] - http://commons.wikimedia.org/wiki/File:SeattleMonorailAccident.jpg
    • By SoRah42 [CC BY-SA 3.0] - http://sorah42.deviantart.com/art/Unique-187278898
    • By Photographer not identified [Public domain], via Wikimedia Commons - http://commons.wikimedia.org/wiki/File:GuateQuake1976BentRailsA.jpg
    • By Studio Lévy and Sons (Studio Lévy & fils) [2] ([1]) [Public domain], via Wikimedia Commons - http://commons.wikimedia.org/wiki/File:Train_wreck_at_Montparnasse_1895.jpg
    • By John McNab [CC BY-NC-SA 2.0] - http://www.flickr.com/photos/johnmcnab/6511819727/
    • http://commons.wikimedia.org/wiki/File:Loket_vid_j%C3%A4rnv%C3%A4gsolyckan_i_Get%C3%A5_1918.jpg
    • By A.J. Russell (American, 1830 - 1902) (1830 - 1902) (American) (photographer, Details of artist on Google Art Project) (Google Art Project: Home - pic) [Public domain], via Wikimedia Commons - http://commons.wikimedia.org/wiki/File:A.J._Russell_(American_-_Railroad_Accident_Caused_by_Rebels_-_Google_Art_Project.jpg
    • By SCFiasco [CC BY-NC-SA 2.0] - http://www.flickr.com/photos/scfiasco/4490322916/

Q & A





Code: https://github.com/UncleGene/concur

Slides: http://203.softover.com/concur/rency