$30 off During Our Annual Pro Sale. View Details »

Just in time RailsIsrael

Just in time RailsIsrael

Talking about time zones in Ruby and Rails and working through a case study in time zones

Elle Meredith

November 24, 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 Slide

  2. Just in time

    View Slide

  3. Just in time

    View Slide

  4. Just in time

    View Slide

  5. Just in time

    View Slide

  6. Today
    — Touch on time related libraries
    — Set up Rails to work with user's time zone
    — Play with Time in Rails
    — Introduce a feature and work through it
    Just in time

    View Slide

  7. Time
    DateTime
    Just in time

    View Slide

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

    View Slide

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

    View Slide

  10. > TZInfo::Timezone.all.count
    => 582
    > TZInfo::Country.get("IL").zone_identifiers
    => ["Asia/Jerusalem"]
    Just in time

    View Slide

  11. > TZInfo::Timezone.all.count
    => 582
    > TZInfo::Country.get("IL").zone_identifiers
    => ["Asia/Jerusalem"]
    > TZInfo::Country.get("AU").zone_identifiers
    => ["Australia/Lord_Howe",
    "Antarctica/Macquarie",
    "Australia/Hobart",
    ...
    Just in time

    View Slide

  12. > TZInfo::Timezone.all.count
    => 582
    > TZInfo::Country.get("IL").zone_identifiers
    => ["Asia/Jerusalem"]
    > 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 Slide

  13. ActiveSupport
    ::TimeZone
    Just in time

    View Slide

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

    View Slide

  15. Friendlier display
    "America/New_York" =>
    "Eastern Time (US & Canada)"
    "Asia/Jerusalem" =>
    Jerusalem
    Just in time

    View Slide

  16. ActiveSupport
    ::TimeWithZone
    Just in time

    View Slide

  17. $ 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 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 Slide

  22. Stick with UTC
    Just in time

    View Slide

  23. Custom user time zone
    create_table :users do |t|
    t.string :time_zone, default: "UTC"
    ...
    end
    Just in time

    View Slide

  24. No enums
    Yes strings
    Just in time

    View Slide

  25. Custom user time zone: forms
    # Simple Form
    <%= f.input :time_zone %>
    Just in time

    View Slide

  26. Custom user time zone: 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 Slide

  27. Custom user time zone: displaying
    <%= time.in_time_zone(curent_user.time_zone) %>
    Just in time

    View Slide

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

    View Slide

  29. 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 Slide

  30. 3 different times
    — System time
    — Application time
    — Database time
    Just in time

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. > 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 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
    Just in time

    View 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
    Just in time

    View Slide

  37. > 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 Slide

  38. > 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 Slide

  39. > 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 Slide

  40. > 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 Slide

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

    View Slide

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

    View Slide

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

    View 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
    Just in time

    View Slide

  45. > 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 Slide

  46. 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 Slide

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

    View Slide

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

    View Slide

  49. 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 Slide

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

    View Slide

  51. 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 Slide

  52. Recent
    Project
    Just in time

    View Slide

  53. First go
    — Test suites that needed to run daily or weekly,
    — at a set time (1AM or 2AM),
    — and use ResqueScheduler to set the schedule to
    run the background workers.
    Just in time

    View Slide

  54. ScheduleRule
    Just in time

    View Slide

  55. 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 Slide

  56. 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 Slide

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

    View Slide

  58. Second go
    — Test runs according to user's settings, at a specific
    week day, hour, and time zone.
    — ResqueScheduler to run hourly and look for test
    suites that are due to run.
    — The ResqueScheduler to look for test runs where
    the background job failed and thus due to be run as
    well.
    Just in time

    View Slide

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

    View Slide

  60. 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 Slide

  61. 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 Slide

  62. 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 Slide

  63. Can we do better?
    Just in time

    View Slide

  64. Meet hour in utc
    Just in time

    View Slide

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

    View Slide

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

    View Slide

  67. 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 Slide

  68. 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 Slide

  69. 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 Slide

  70. 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 Slide

  71. 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 Slide

  72. Always work
    with UTC
    Just in time

    View Slide

  73. A couple more takeaways
    — Use Time.current or Time.zone.today.
    — Freeze the time in your tests, preferably by using a
    block.
    Just in time

    View Slide

  74. Almost done
    — robots.thoughtbot.com
    — speakerdeck.com/aemeredith
    — @aemeredith
    — https://www.youtube.com/watch?v=-5wpm-gesOY
    Just in time

    View Slide

  75. Thanks!
    Just in time

    View Slide