Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Avatar for Elle Meredith

Elle Meredith

July 20, 2015
Tweet

More Decks by Elle Meredith

Other Decks in Programming

Transcript

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

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

    work with user's time_zone Just in time
  3. 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
  4. 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
  5. > 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
  6. Limit the set of zones provided by TZInfo to a

    meaningful subset of 146 zones Just in time
  7. $ 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
  8. 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
  9. Setting a custom time zone # in console > Time.zone

    = "Perth" # in config/application.rb config.time_zone = "Perth" Just in time
  10. 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
  11. 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
  12. 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
  13. > Time.zone.name => "UTC" > Time.now => 2015-07-04 17:53:23 -0400

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

    > Time.zone = "Fiji" => "Fiji" > Time.zone.name => "Fiji" Just in time
  15. > 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
  16. > 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
  17. > 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
  18. > 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
  19. > 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
  20. > 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
  21. > Time.zone.name => "Fiji" > Date.today => Sat, 04 Jul

    2015 > Time.zone.today => Sun, 05 Jul 2015 Just in time
  22. > 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
  23. > 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. So what was the problem? The scheduled tests were running

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

    a specific week day, hour, and time zone. Just in time
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. Third go 4 Remove the .current_zones method. 4 Introduce the

    :hour_in_utc column in ScheduleRule class. Just in time
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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