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 Slide

  2. 4 Touch on time related libraries
    Just in time

    View Slide

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

    View 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 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 Slide

  6. Time
    DateTime
    Just in time

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 Slide

  12. ActiveSupport
    ::TimeZone
    Just in time

    View Slide

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

    View Slide

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

    View Slide

  15. ActiveSupport
    ::TimeWithZone
    Just in time

    View 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 Slide

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

    View Slide

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

    View Slide

  19. Setting a custom time zone
    # in console
    > Time.zone = "Perth"
    # in config/application.rb
    config.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"
    # ^ the default is "utc"
    Just in time

    View Slide

  21. Stick with UTC
    Just in time

    View Slide

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

    View Slide

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

    View Slide

  24. With user time zones: forms
    # Simple Form

    Just in time

    View 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 Slide

  26. With user time zones: displaying

    Just in time

    View Slide

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

    View 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 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 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 Slide

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

    View Slide

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

    View Slide

  42. > Time.zone.name
    => "Fiji"
    > Date.today
    => Sat, 04 Jul 2015
    > Time.zone.today
    => Sun, 05 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
    > Time.zone.tomorrow
    => Mon, 06 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
    > 1.day.from_now
    => Mon, 06 Jul 2015 10:00:56 FJT +12:00
    Just in time

    View 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 Slide

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

    View Slide

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

    View 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 Slide

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

    View 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 Slide

  51. Recent
    Project
    Just in time

    View 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 Slide

  53. ScheduleRule
    Just in time

    View 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 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 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 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 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 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 Slide

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

    View 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 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 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 Slide

  64. Can we do better?
    Just in time

    View Slide

  65. Meet hour in utc
    Just in time

    View Slide

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

    View Slide

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

    View 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 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 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 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 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 Slide

  73. Always work
    with UTC
    Just in time

    View 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 Slide

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

    View Slide