Just in Time
A case study in time zones
Elle Meredith
@aemeredith
Just in time
Slide 2
Slide 2 text
4 Touch on time related libraries
Just in time
Slide 3
Slide 3 text
4 Touch on time related libraries
4 Setup Rails to work with user's time_zone
Just in time
Slide 4
Slide 4 text
4 Touch on time related libraries
4 Setup Rails to work with user's time zone
4 Play with Time in Rails
Just in time
Slide 5
Slide 5 text
4 Touch on time related libraries
4 Setup Rails to work with user's time zone
4 Play with Time in Rails
4 Introduce a feature and work through it
Just in time
Slide 6
Slide 6 text
Time
DateTime
Just in time
Slide 7
Slide 7 text
TZInfo
Ruby timezone library, which provides daylight savings
aware transformations between times in different
timezones.
Just in time
Slide 8
Slide 8 text
> TZInfo::Timezone.all.count
=> 582
Just in time
Slide 9
Slide 9 text
> TZInfo::Timezone.all.count
=> 582
> TZInfo::Country.get("GB").zone_identifiers
=> ["Europe/London"]
Just in time
Slide 10
Slide 10 text
> TZInfo::Timezone.all.count
=> 582
> TZInfo::Country.get("GB").zone_identifiers
=> ["Europe/London"]
> TZInfo::Country.get("AU").zone_identifiers
=> ["Australia/Lord_Howe",
"Antarctica/Macquarie",
"Australia/Hobart",
...
Just in time
Slide 11
Slide 11 text
> TZInfo::Timezone.all.count
=> 582
> TZInfo::Country.get("GB").zone_identifiers
=> ["Europe/London"]
> TZInfo::Country.get("AU").zone_identifiers
=> ["Australia/Lord_Howe",
"Antarctica/Macquarie",
"Australia/Hobart",
...
> TZInfo::Country.get("US").zone_identifiers.count
=> 29
Just in time
Slide 12
Slide 12 text
ActiveSupport
::TimeZone
Just in time
Slide 13
Slide 13 text
Limit the set of zones provided by
TZInfo to a meaningful subset of
146 zones
Just in time
Slide 14
Slide 14 text
Friendlier zones
"America/New_York" =>
"Eastern Time (US & Canada)"
Just in time
Slide 15
Slide 15 text
ActiveSupport
::TimeWithZone
Just in time
Slide 16
Slide 16 text
$ rake time:zones:all
* UTC -11:00 *
American Samoa
International Date Line West
Midway Island
Samoa
* UTC -10:00 *
Hawaii
* UTC -09:00 *
Alaska
...
Just in time
Slide 17
Slide 17 text
Checking current time zone
# in console
> Time.zone
=> #>>,
@name="UTC",
@tzinfo=#,
@utc_offset=nil>
Just in time
Slide 18
Slide 18 text
Setting a custom time zone
# in console
> Time.zone = "Perth"
Just in time
Slide 19
Slide 19 text
Setting a custom time zone
# in console
> Time.zone = "Perth"
# in config/application.rb
config.time_zone = "Perth"
Just in time
Slide 20
Slide 20 text
Setting a custom time zone
# in console
> Time.zone = "Perth"
# in config/application.rb
config.time_zone = "Perth"
# ^ the default is "utc"
Just in time
Slide 21
Slide 21 text
Stick with UTC
Just in time
Slide 22
Slide 22 text
With user time zones
create_table :users do |t|
t.string :time_zone, default: "UTC"
...
end
Just in time
Slide 23
Slide 23 text
With user time zones
4 No: enums
4 Yes: strings
Just in time
Slide 24
Slide 24 text
With user time zones: forms
# Simple Form
<%= f.input :time_zone %>
Just in time
Slide 25
Slide 25 text
With user time zones: setting
# app/controllers/application_controller.rb
around_action :set_time_zone, if: :current_user
private
def set_time_zone(&block)
Time.use_zone(current_user.time_zone, &block)
end
Just in time
Slide 26
Slide 26 text
With user time zones: displaying
<%= time.in_time_zone(curent_user.time_zone) %>
Just in time
Slide 27
Slide 27 text
ISO8601 and APIs
> time = Time.now.utc.iso8601
=> "2015-07-04T21:53:23Z"
Just in time
Slide 28
Slide 28 text
ISO8601 and APIs
> time = Time.now.utc.iso8601
=> "2015-07-04T21:53:23Z"
> Time.iso8601(time)
=> 2015-07-04 21:53:23 UTC
Just in time
Slide 29
Slide 29 text
3 different times
4 System time
4 Application time
4 Database time
Just in time
Slide 30
Slide 30 text
> Time.zone.name
=> "UTC"
Just in time
Slide 31
Slide 31 text
> Time.zone.name
=> "UTC"
> Time.now
=> 2015-07-04 17:53:23 -0400
Just in time
Slide 32
Slide 32 text
> Time.zone.name
=> "UTC"
> Time.now
=> 2015-07-04 17:53:23 -0400
> Time.zone = "Fiji"
=> "Fiji"
Just in time
Slide 33
Slide 33 text
> Time.zone.name
=> "UTC"
> Time.now
=> 2015-07-04 17:53:23 -0400
> Time.zone = "Fiji"
=> "Fiji"
> Time.zone.name
=> "Fiji"
Just in time
Slide 34
Slide 34 text
> Time.zone.name
=> "UTC"
> Time.now
=> 2015-07-04 17:53:23 -0400
> Time.zone = "Fiji"
=> "Fiji"
> Time.zone.name
=> "Fiji"
> Time.now
=> 2015-07-04 17:53:37 -0400
Just in time
Time zone related querying
Post.where("published_at > ?", Time.current)
# SELECT "posts".* FROM "posts"
# WHERE (published_at >
# '2015-07-04 17:45:01.452465')
Just in time
Slide 46
Slide 46 text
DON'T
* Time.now
DO
* Time.current
* 2.hours.ago
Just in time
Slide 47
Slide 47 text
DON'T
* Date.today
* Date.today.to_time
DO
* Time.zone.today
* 1.day.from_now
Just in time
Slide 48
Slide 48 text
DON'T
* Time.parse("2015-07-04 17:05:37")
* Time.strptime(string, "%Y-%m-%dT%H:%M:%S%z")
DO
* Time.zone.parse("2015-07-04 17:05:37")
* Time.strptime(
string,
"%Y-%m-%dT%H:%M:%S%z"
).in_time_zone
Just in time
Slide 49
Slide 49 text
Testing time zones
ActiveSupport::Testing::TimeHelpers
travel_to 1.day do
# do something tomorrow
end
travel_back
Just in time
Slide 50
Slide 50 text
Testing time zones: gems
# Timecop
Timecop.freeze new_time
Timecop.travel new_time
Timecop.return
Time.use_zone("Sydney") do … end
# Delorean
Delorean.time_travel_to("1 month ago") do … end
Delorean.back_to_the_present
# Zonebie
Zonebie.set_random_timezone
Just in time
Slide 51
Slide 51 text
Recent
Project
Just in time
Slide 52
Slide 52 text
First go
4 Test suites that needed to run daily or weekly,
4 at a set time (1AM or 2AM),
4 and use ResqueScheduler to set the schedule to run
the background workers.
Just in time
Slide 53
Slide 53 text
ScheduleRule
Just in time
Slide 54
Slide 54 text
ResqueScheduler
# config/resque_schedule.yml
weekly_test:
cron: "0 1 * * 0"
class: ScheduledWeeklyRunsWorker
args:
description: 'Run test suites weekly'
daily_test:
cron: "0 2 * * *"
class: ScheduledDailyRunsWorker
args:
description: 'Run test suites daily'
Just in time
Slide 55
Slide 55 text
Cron Syntax
* * * * * command to execute
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ │
│ │ │ │ └───── day of week (0 - 7) from Sunday
│ │ │ └────────── month (1 - 12)
│ │ └─────────────── day of month (1 - 31)
│ └──────────────────── hour (0 - 23)
└───────────────────────── min (0 - 59)
Just in time
Slide 56
Slide 56 text
So what was the problem?
The scheduled tests were running at 2AM AEST
regardless of the user’s time zone.
Just in time
Slide 57
Slide 57 text
Second go
4 Test runs according to user's settings, at a specific
week day, hour, and time zone.
Just in time
Slide 58
Slide 58 text
Second go
4 Test runs according to user's settings, at a specific
week day, hour, and time zone.
4 ResqueScheduler to run hourly and look for test
suites that are due to run.
Just in time
Slide 59
Slide 59 text
Second go
4 Test runs according to user's settings, at a specific
week day, hour, and time zone.
4 ResqueScheduler to run hourly and look for test
suites that are due to run.
4 The ResqueScheduler to look for test runs where the
background job failed and thus due to be run as well.
Just in time
Slide 60
Slide 60 text
ScheduleRule.rb
every: string,
wday: integer,
hour: integer,
time_zone: string,
last_scheduled_run_at: timestamp,
suite: references,
...
Just in time
Slide 61
Slide 61 text
Still second go
# lib/extensions.rb
module ActiveSupport
class TimeZone
def self.current_zones(hour)
all.select { |zone|
t = Time.current.in_time_zone(zone)
t.hour == hour
}.map(&:tzinfo).map(&:name)
end
end
end
Just in time
Slide 62
Slide 62 text
Still second go
# app/models/schedule_rule.rb
class ScheduleRule < ActiveRecord::Base
def self.find_rules(options={})
hour = options[:hour]
where(zone: ActiveSupport::TimeZone.current_zones(hour)).
where(options)
end
def self.suites_to_run(time_ago=Time.current, options={})
find_rules(options).
older_than(time_ago).
map(&:suite)
end
end
Just in time
Slide 63
Slide 63 text
Still second go
# app/workers/scheduled_runs_worker.rb
class ScheduledRunsWorker
def self.perform
ScheduleRule.
run_scheduled.
daily.
suites_to_run(yesterday, {hour: Time.now.utc.hour})
end
end
Just in time
Slide 64
Slide 64 text
Can we do better?
Just in time
Slide 65
Slide 65 text
Meet hour in utc
Just in time
Slide 66
Slide 66 text
Third go
4 Remove the .current_zones method.
4 Introduce the :hour_in_utc column in ScheduleRule
class.
Just in time
Slide 67
Slide 67 text
Third go
class AddHourInUtc < ActiveRecord::Migration
def change
add_column :schedule_rules, :hour_in_utc, :integer
end
end
Just in time
Slide 68
Slide 68 text
Still third go
# app/models/schedule_rule.rb
class ScheduleRule < ActiveRecord::Base
before_save :set_hour_in_utc
private
def set_hour_in_utc
self.hour_in_utc =
ActiveSupport::TimeZone[zone].
parse("#{hour}:00:00").
utc.
hour
end
end
Just in time
Slide 69
Slide 69 text
Still third go
# app/models/schedule_rule.rb
class ScheduleRule < ActiveRecord::Base
def self.suites_to_run(time_ago=Time.current, options={})
where(options).
older_than(time_ago).
map(&:suite)
end
end
Just in time
Slide 70
Slide 70 text
Worker didn't change
# app/workers/scheduled_runs_worker.rb
class ScheduledRunsWorker
def self.perform
ScheduleRule.
run_scheduled.
daily.
suites_to_run(yesterday, {hour: Time.now.utc.hour})
end
end
Just in time
Slide 71
Slide 71 text
Previously
# app/models/schedule_rule.rb
class ScheduleRule < ActiveRecord::Base
def self.find_rules(options={})
hour = options[:hour]
where(zone: ActiveSupport::TimeZone.current_zones(hour)).
where(options)
end
def self.suites_to_run(time_ago=Time.current, options={})
find_rules(options).
older_than(time_ago).
map(&:suite)
end
end
Just in time
Slide 72
Slide 72 text
Currently
class ScheduleRule < ActiveRecord::Base
def self.suites_to_run(time_ago=Time.current, options={})
where(options).
older_than(time_ago).
map(&:suite)
end
end
Just in time
Slide 73
Slide 73 text
Always work
with UTC
Just in time
Slide 74
Slide 74 text
A couple more takeaways
4 Use Time.current or Time.zone.today.
4 Use testing helper methods of your choice to freeze
the time in your tests, preferably by using a block.
Just in time
Slide 75
Slide 75 text
Thanks!
4 robots.thoughtbot.com
4 speakerdeck.com/aemeredith
4 @aemeredith
Just in time