1

I have implemented a solution similar to this to prune my database.

# model.rb
after_create do
  self.class.prune(ENV['VARIABLE_NAME'])
end
def self.prune(max)
  order('created_at DESC').last.destroy! until count <= max
end

This works well in manual testing.

In RSpec, the test looks like this:

# spec/models/model_spec.rb
before(:each) do
  @model = Model.new
end

describe "prune" do
  it "should prune the database when it becomes larger than the allowed size" do
    25.times { create(:model) }

    first_model = model.first
    expect{create(:model)}.to change{Model.count}.by(0)
    expect{Model.find(first_model.id)}.to raise_error(ActiveRecord::RecordNotFound)
    end
  end
end

The result is

  1) Model prune should prune the database when it becomes larger than the allowed size
     Failure/Error: expect{Model.find(first_model.id)}.to raise_error(ActiveRecord::RecordNotFound)
       expected ActiveRecord::RecordNotFound but nothing was raised

Inspecting the database during the test execution reveals that the call to order('created_at DESC').last is yielding the first instance of the model created in the 25.times block (Model#2) and not the model created in the before(:each) block (Model#1).

If I change the line

25.times { create(:model) }

to

25.times { sleep(1); create(:model) }

the test passes. If I instead sleep(0.1), the test still fails.

Does this mean that if my app creates two or more Model instances within 1 second of each other that it will choose the newest among them when choosing which to destroy (as opposed to the oldest, which is the intended behavior)? Could this be an ActiveRecord or MySQL bug?

Or if not, is there something about the way FactoryGirl or RSpec create records that isn't representative of production? How can I be sure my test represents realistic scenarios?

Community
  • 1
  • 1
Sam
  • 1,205
  • 1
  • 21
  • 39

1 Answers1

1

If the precision of your time column is only one second then you can't distinguish between items created in the same second (when sorting by date only).

If this is a concern in production then you could sort on created_at and id to enforce a deterministic order. From MySQL 5.6 onwards you can also create datetime columns that store fractional seconds. This doesn't eliminate the problem, but it would happen less often.

If it's just in tests then you can also fake time. As of rails 4.1 (I think) active support has the travel test helpers and there is also the timecop gem.

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • Thank you for that. I (very foolishly) trusted MySQL to store and ActiveRecord to manage millisecond date times. As you say, it does from v 5.6, but of course I'm on 5.5 ... which seems like the stable version for CentOS 7(?). I like your suggestion of sorting on id as well as created_at, that is good enough. Thanks again – Sam Apr 08 '16 at 13:23