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

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