Hello, All:
I am grappling with an interesting problem that I can’t believe I’m the first to tackle. Hoping someone has covered this ground before, or is experienced enough to give good suggestions, and can suggest a reasonable path forward.
The issue is related to transactions and ActiveRecord::Timestamp.
Specifically, if we insert or update multiple rows in a single transaction, each row gets a slightly different updated_at/created_at timestamp.
The reason is because instead of using the SQL function NOW() to set the created/updated times, the ruby code generates the timestamps at the time the SQL for the insert/update is created.
The problem is that we need the timestamps on rows added/updated in the same transaction to match (for auditing and perceived atomicity reasons).
As I allude to above, if ActiveRecord used the sql function NOW() to set the created_on/updated_on times, they would all be the time that the database server began the transaction. (See now() explanation at e.g. PostgreSQL: Documentation: 8.2: Date/Time Functions and Operators )
But I understand that setting updated_on/created_on times to NOW() is tricky through ActiveRecord.
Here are some ideas we’ve thrown around to fix this, and our thoughts on each:
Idea 1: Fetch NOW() from the database at the beginning of the transaction, and use that verbatim for all updated_on/created_on times that need to get set
Thoughts: inconsistent with the Rails model, which always uses the time on the ruby side, not the database. Also adds a roundtrip to the database.
Idea 2: Mixin new modules to ActiveRecord::Base to override the transaction() call, record the time, and then call super(). Also override the Timestamp current_time_from_proper_timezone method to return the time that our transaction() call squirrelled away. Theres a little more to this (clearing the timestamp when the transaction is committed or rolled back), but you get the idea
Thoughts: Modifies ActiveRecord::Base to be more different than might be obvious. We think we prefer the next idea (3).
Idea 3: Subclass ActiveRecord::Base (to say ActiveRecordLocal) and mix in the modules mentioned above into this class.
Thoughts: This makes is clearer that when you’re using ActiveRecordLocal, you’re not getting the stock ActiveRecord::Base behavior. Otherwise this solution is identical to Idea 2.
Idea 4: when then transaction starts, use Timecop::freeze to freeze the time that ruby sees.
Thoughts: We can’t freeze the time for all code in the app while a transaction is pending. Plus Timecop is for testing, not production use-- but most importantly see previous point.
We’ve seen other ideas which we rejected and won’t bring up here.
So, after all this motivation – the questions;
Q1: Has anyone tacked this issue before? How did you solve it? Is there an easy way to accomplish our goal?
Q2: What strategy do you think we should take to fix this issue? (Either one explained above, or another strategy)?
At this point we’re moving forward with Idea 3, but I’d love to hear some experienced people’s input on this topic
Best,
“RubyRailHead”