We all live in different timezone in this world just as our users use our applications in different ones. But what does it mean for ROR applications?

What is the application’s timezone?

Ever wondered how rails uses timezone for the timestamp displayed on UI?

Configuration in application.rb as given below will set the timezone for application and active record(database) respectively.

class Application < Rails::Application
  config.time_zone = 'Pacific Time (US & Canada)'
  config.active_record.default_timezone = :utc
  ......
end

With the above setting, the data with DateTime data type stored in DB will have UTC time zone and the data requested using ORM will have Pacific Time zone.

How do we work with different timezone?

There are scenarios where we need to execute the query in the user’s time zone OR respond with the time information in other than that of the application’s timezone. One way of handling timezone change for ActiveSupport::TimeWithZone object is by using its method in_time_zone which returns the simultaneous time in the zone passed.

But what if our logic needs a change in timezone for any block of code? It’s not a good idea to change the timezone by assigning the value during the execution as it is for the thread and can affect the subsequent executions. Following is the example code which will lead to time zone leaks.

The subsequent executions after evaluating the “Time.zone = current_user.time_zone” code will change the thread’s time zone to that of the user’s. And this is not what we expected.

# Bad code
class Api::ActorsController < ApplicationController
  before_action :set_time_zone, only: [:my_listings]
  def my_listings
    @list = current_user.lists
    @execution_time_zone = Time.zone
  end
  def index
    @actors = Actor.all
    @execution_time_zone = Time.zone
  end
  def set_time_zone
    Time.zone = current_user.time_zone
  end
end

How do we do it right?

With the help of Time’s use_zone method, we can change the time zone for any particular block of code. This will help us safely change the timezone only for the part of the code which we want to execute in the user’s time zone.

# Good code without time zone leaks
class Api::ActorsController < ApplicationController
  around_action :set_time_zone, only: [:my_listings]
  def my_listings
    @list = current_user.lists
    @execution_time_zone = Time.zone
  end

  def index
    @actors = Actor.all
    @execution_time_zone = Time.zone
  end

  def set_time_zone
    time_zone = current_user.time_zone
    if time_zone.present?
      Time.use_zone(time_zone) { yield }
    else
      yield
    end
  end
end

The logic here makes sure to consider the current_user’s time zone if present otherwise it’ll use the application’s time zone. You can write the filter method set_time_zone in ApplicationController if it’s used across controllers.

Are we good here with our implementation? Guess that’s not it.

How can we make sure we test it right?

Yes, we do make sure there are test cases for the code we write. But the important question would be are we making sure the prerequisites are correct for our test cases and are we testing the code we have written correctly?

With our code logic having time zone change and based on how we write our pre-requisite in our test suite, it can impact existing test cases. Dang!

And the question arises “How do we test it right? “

What timezone is it? Test case failure meme.
The agony of test case failures during production deployment

There are ways to change the time zone and reset it after the execution of each test case. But there are chances the developer forgets to reset the time zone back. And also when tests are run in parallel there’s a chance that timezone leaks can cause test case failures.

Many developers fall for the easy-peasy way of setting the time zone. i.e,

before(:each) do
  Time.zone = current_user.time_zone
end
-------OR-----
before(:all) do
  Time.zone = current_user.time_zone
end
.... Test cases ...
after(:all) do
  Time.zone = 'Pacific Time (US & Canada)' # Assuming it to be application timezone
end

The above code seems straightforward. Although it can lead to failures in test cases if run in parallel, OR when it’s missed to change the time zone back.

To make things simpler we can make use of helper methods. Eventually, it can be used with any block of our test case i.e, describe, context, or it block.

# Inside your spec_helper.rb file you can add this configuration which acts as around filter
config.around(:each, :tz_change) do |example|
  Time.use_zone(example.metadata[:tz_change]) do
    example.run
  end
end

# And in the code it can be used as follows
# spec/api/actors_controller_spec.rb
describe "my_listings", tz_change => 'Atlantic Time (Canada)' do
  let(:user)    { FactoryBot.create(:user, name: 'FooBar')}
  let(:listing) { FactoryBot.create(:listing, user: user, name: 'Listing cars')}
  it "Should return my listings" do
   # The time zone of object will be Atlantic Time (Canada) here
  end
end

This helper acted as a savior for us to test our code more efficiently. Also, future test cases followed the trend of using helper methods. As a result, this avoided any intermittent failures due to the assignment of timezone.

References – timezone

https://thoughtbot.com/blog/its-about-time-zones
https://blog.kiprosh.com/working-with-rails-time-zone/

 

Get to know about Rently at https://use.rently.com/

To learn more about Engineering topics visit – https://engineering.rently.com

This Post Has One Comment

Leave a Reply

Login with