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

    View full-size slide

  2. 4 Touch on time related libraries
    Just in time

    View full-size slide

  3. 4 Touch on time related libraries
    4 Setup Rails to work with user's time_zone
    Just in time

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  6. Time
    DateTime
    Just in time

    View full-size slide

  7. TZInfo
    Ruby timezone library, which provides daylight savings
    aware transformations between times in different
    timezones.
    Just in time

    View full-size slide

  8. > TZInfo::Timezone.all.count
    => 582
    Just in time

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  12. ActiveSupport
    ::TimeZone
    Just in time

    View full-size slide

  13. Limit the set of zones provided by
    TZInfo to a meaningful subset of
    146 zones
    Just in time

    View full-size slide

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

    View full-size slide

  15. ActiveSupport
    ::TimeWithZone
    Just in time

    View full-size slide

  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

    View full-size slide

  17. Checking current time zone
    # in console
    > Time.zone
    => #@current_period=#nil,nil,#>>,
    @name="UTC",
    @tzinfo=#,
    @utc_offset=nil>
    Just in time

    View full-size slide

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

    View full-size slide

  19. Setting a custom time zone
    # in console
    > Time.zone = "Perth"
    # in config/application.rb
    config.time_zone = "Perth"
    Just in time

    View full-size slide

  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

    View full-size slide

  21. Stick with UTC
    Just in time

    View full-size slide

  22. With user time zones
    create_table :users do |t|
    t.string :time_zone, default: "UTC"
    ...
    end
    Just in time

    View full-size slide

  23. With user time zones
    4 No: enums
    4 Yes: strings
    Just in time

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  26. With user time zones: displaying
    <%= time.in_time_zone(curent_user.time_zone) %>
    Just in time

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  29. 3 different times
    4 System time
    4 Application time
    4 Database time
    Just in time

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  40. > Time.zone.name
    => "Fiji"
    Just in time

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  46. DON'T
    * Time.now
    DO
    * Time.current
    * 2.hours.ago
    Just in time

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  49. Testing time zones
    ActiveSupport::Testing::TimeHelpers
    travel_to 1.day do
    # do something tomorrow
    end
    travel_back
    Just in time

    View full-size slide

  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

    View full-size slide

  51. Recent
    Project
    Just in time

    View full-size slide

  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

    View full-size slide

  53. ScheduleRule
    Just in time

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  56. So what was the problem?
    The scheduled tests were running at 2AM AEST
    regardless of the user’s time zone.
    Just in time

    View full-size slide

  57. Second go
    4 Test runs according to user's settings, at a specific
    week day, hour, and time zone.
    Just in time

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  60. ScheduleRule.rb
    every: string,
    wday: integer,
    hour: integer,
    time_zone: string,
    last_scheduled_run_at: timestamp,
    suite: references,
    ...
    Just in time

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  64. Can we do better?
    Just in time

    View full-size slide

  65. Meet hour in utc
    Just in time

    View full-size slide

  66. Third go
    4 Remove the .current_zones method.
    4 Introduce the :hour_in_utc column in ScheduleRule
    class.
    Just in time

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  73. Always work
    with UTC
    Just in time

    View full-size slide

  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

    View full-size slide

  75. Thanks!
    4 robots.thoughtbot.com
    4 speakerdeck.com/aemeredith
    4 @aemeredith
    Just in time

    View full-size slide