Just in time

Just in time

An overview of time zones in Rails. And handle multiple user-defined time zones in Rails with scheduled background workers, with an example of a time zones feature.

Presented at Ruby Brighton, July 20, 2015

492d339a2ec66fa8d80e937abddb58e6?s=128

Elle Meredith

July 20, 2015
Tweet

Transcript

  1. Just in Time A case study in time zones Elle

    Meredith @aemeredith Just in time
  2. 4 Touch on time related libraries Just in time

  3. 4 Touch on time related libraries 4 Setup Rails to

    work with user's time_zone Just in time
  4. 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
  5. 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
  6. Time DateTime Just in time

  7. TZInfo Ruby timezone library, which provides daylight savings aware transformations

    between times in different timezones. Just in time
  8. > TZInfo::Timezone.all.count => 582 Just in time

  9. > TZInfo::Timezone.all.count => 582 > TZInfo::Country.get("GB").zone_identifiers => ["Europe/London"] Just in

    time
  10. > 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
  11. > 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
  12. ActiveSupport ::TimeZone Just in time

  13. Limit the set of zones provided by TZInfo to a

    meaningful subset of 146 zones Just in time
  14. Friendlier zones "America/New_York" => "Eastern Time (US & Canada)" Just

    in time
  15. ActiveSupport ::TimeWithZone Just in time

  16. $ 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
  17. Checking current time zone # in console > Time.zone =>

    #<ActiveSupport::TimeZone:0x007fbf46947b38 @current_period=#<TZInfo::TimezonePeriod: nil,nil,#<TZInfo::TimezoneOffset: 0,0,UTC>>>, @name="UTC", @tzinfo=#<TZInfo::TimezoneProxy: Etc/UTC>, @utc_offset=nil> Just in time
  18. Setting a custom time zone # in console > Time.zone

    = "Perth" Just in time
  19. Setting a custom time zone # in console > Time.zone

    = "Perth" # in config/application.rb config.time_zone = "Perth" Just in time
  20. 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
  21. Stick with UTC Just in time

  22. With user time zones create_table :users do |t| t.string :time_zone,

    default: "UTC" ... end Just in time
  23. With user time zones 4 No: enums 4 Yes: strings

    Just in time
  24. With user time zones: forms # Simple Form <%= f.input

    :time_zone %> Just in time
  25. 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
  26. With user time zones: displaying <%= time.in_time_zone(curent_user.time_zone) %> Just in

    time
  27. ISO8601 and APIs > time = Time.now.utc.iso8601 => "2015-07-04T21:53:23Z" Just

    in time
  28. 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
  29. 3 different times 4 System time 4 Application time 4

    Database time Just in time
  30. > Time.zone.name => "UTC" Just in time

  31. > Time.zone.name => "UTC" > Time.now => 2015-07-04 17:53:23 -0400

    Just in time
  32. > Time.zone.name => "UTC" > Time.now => 2015-07-04 17:53:23 -0400

    > Time.zone = "Fiji" => "Fiji" Just in time
  33. > Time.zone.name => "UTC" > Time.now => 2015-07-04 17:53:23 -0400

    > Time.zone = "Fiji" => "Fiji" > Time.zone.name => "Fiji" Just in time
  34. > 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
  35. > 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 > Time.zone.now Just in time
  36. > 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 > Time.zone.now => Sun, 05 Jul 2015 09:53:42 FJT +12:00 Just in time
  37. > Time.zone = "Fiji" => "Fiji" > Time.zone.name => "Fiji"

    > Time.now => 2015-07-04 17:53:37 -0400 > Time.zone.now => Sun, 05 Jul 2015 09:53:42 FJT +12:00 > Time.current Just in time
  38. > Time.zone.name => "Fiji" > Time.now => 2015-07-04 17:53:37 -0400

    > Time.zone.now => Sun, 05 Jul 2015 09:53:42 FJT +12:00 > Time.current => Sun, 05 Jul 2015 09:54:17 FJT +12:00 Just in time
  39. > Time.zone = "Fiji" => "Fiji" > Time.zone.name => "Fiji"

    > Time.now => 2015-07-04 17:53:37 -0400 > Time.zone.now => Sun, 05 Jul 2015 09:53:42 FJT +12:00 > Time.current => Sun, 05 Jul 2015 09:54:17 FJT +12:00 > Time.now.in_time_zone => Sun, 05 Jul 2015 09:56:57 FJT +12:00 Just in time
  40. > Time.zone.name => "Fiji" Just in time

  41. > Time.zone.name => "Fiji" > Date.today => Sat, 04 Jul

    2015 Just in time
  42. > Time.zone.name => "Fiji" > Date.today => Sat, 04 Jul

    2015 > Time.zone.today => Sun, 05 Jul 2015 Just in time
  43. > Time.zone.name => "Fiji" > Date.today => Sat, 04 Jul

    2015 > Time.zone.today => Sun, 05 Jul 2015 > Time.zone.tomorrow => Mon, 06 Jul 2015 Just in time
  44. > Time.zone.name => "Fiji" > Date.today => Sat, 04 Jul

    2015 > Time.zone.today => Sun, 05 Jul 2015 > Time.zone.tomorrow => Mon, 06 Jul 2015 > 1.day.from_now => Mon, 06 Jul 2015 10:00:56 FJT +12:00 Just in time
  45. 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
  46. DON'T * Time.now DO * Time.current * 2.hours.ago Just in

    time
  47. DON'T * Date.today * Date.today.to_time DO * Time.zone.today * 1.day.from_now

    Just in time
  48. 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
  49. Testing time zones ActiveSupport::Testing::TimeHelpers travel_to 1.day do # do something

    tomorrow end travel_back Just in time
  50. 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
  51. Recent Project Just in time

  52. 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
  53. ScheduleRule Just in time

  54. 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
  55. 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
  56. So what was the problem? The scheduled tests were running

    at 2AM AEST regardless of the user’s time zone. Just in time
  57. Second go 4 Test runs according to user's settings, at

    a specific week day, hour, and time zone. Just in time
  58. 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
  59. 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
  60. ScheduleRule.rb every: string, wday: integer, hour: integer, time_zone: string, last_scheduled_run_at:

    timestamp, suite: references, ... Just in time
  61. 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
  62. 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
  63. 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
  64. Can we do better? Just in time

  65. Meet hour in utc Just in time

  66. Third go 4 Remove the .current_zones method. 4 Introduce the

    :hour_in_utc column in ScheduleRule class. Just in time
  67. Third go class AddHourInUtc < ActiveRecord::Migration def change add_column :schedule_rules,

    :hour_in_utc, :integer end end Just in time
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. Always work with UTC Just in time

  74. 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
  75. Thanks! 4 robots.thoughtbot.com 4 speakerdeck.com/aemeredith 4 @aemeredith Just in time