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

Slightly Less Painful Time Zones (RailsConf)

Slightly Less Painful Time Zones (RailsConf)

Abstract:
For developers, there are two things that are certain for time zones: you can’t avoid having to deal with them, and you will screw them up at some point. There are, however, some ways to mitigate the pain. This talk will discuss tactics for avoiding time zone mayhem, using a feature to send out weekly email reports in a customer’s local time zone as a case study. It will cover idiosyncrasies of how time zones are handled in Ruby and Rails, how to write tests to avoid false positives, and advice on how to release time zone-related code changes more safely.

This talk was presented at RailsConf 2015 in Atlanta, ~30 minutes: https://www.youtube.com/watch?v=VFDurYw6aZ8
Blog post: http://kwugirl.blogspot.com/2015/04/slightly-less-painful-time-zones.html

40be222374e709cae7543dee233fe2e1?s=128

Katherine Wu

April 21, 2015
Tweet

Transcript

  1. Slightly Less Painful Time Zones Katherine Wu (KWu) @kwugirl. !

    ! Software Engineer
  2. ! @kwugirl why time is hard the problem the proposed

    solution the actual solution
  3. ! @kwugirl why time is hard http://xkcd.com/1514/

  4. ! @kwugirl https://twitter.com/endtwist/status/544598751449739265

  5. ! @kwugirl wat

  6. ! @kwugirl “in October 1582, since days 5..14 just do

    not exist.” http://guides.rubyonrails.org/v4.0.13/active_support_core_extensions.html#extensions-to-date
  7. ! @kwugirl wat

  8. ! @kwugirl

  9. ! @kwugirl

  10. ! @kwugirl falsehoods programmers believe about time http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

  11. ! @kwugirl falsehoods programmers believe about time http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time

  12. ! @kwugirl falsehoods programmers believe about time • There are

    only 24 time zones. http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
  13. ! @kwugirl falsehoods programmers believe about time • There are

    only 24 time zones. • Time zones always differ by a whole hour. http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
  14. ! @kwugirl falsehoods programmers believe about time • There are

    only 24 time zones. • Time zones always differ by a whole hour. • Time always goes forwards. http://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time http://infiniteundo.com/post/25509354022/more-falsehoods-programmers-believe-about-time
  15. ! @kwugirl

  16. ! @kwugirl

  17. ! @kwugirl wat

  18. ! @kwugirl Lesson #1: Trust NOTHING

  19. ! @kwugirl Lesson #1: Trust NOTHING (including Argentina)

  20. ! @kwugirl

  21. ! @kwugirl [1] pry(main)> 1.month == 30.days

  22. ! @kwugirl [1] pry(main)> 1.month == 30.days => true

  23. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today
  24. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015
  25. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month
  26. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month => Mon, 09 Mar 2015
  27. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month => Mon, 09 Mar 2015 [4] pry(main)> Date.today - 30.days
  28. ! @kwugirl [1] pry(main)> 1.month == 30.days => true [2]

    pry(main)> Date.today => Thu, 09 Apr 2015 [3] pry(main)> Date.today - 1.month => Mon, 09 Mar 2015 [4] pry(main)> Date.today - 30.days => Tue, 10 Mar 2015
  29. ! @kwugirl Lesson #1: Trust NOTHING (including Argentina)

  30. ! @kwugirl Lesson #1: Trust NOTHING (including Argentina) (including math)

  31. ! @kwugirl the problem

  32. ! @kwugirl weekly email report

  33. ! @kwugirl

  34. ! @kwugirl cron job

  35. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… enqueues
  36. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… email for account 1 email for account 2 email for account 3… enqueues generates
  37. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… email for account 1 email for account 2 email for account 3… enqueues SCHEDULING generates
  38. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… email for account 1 email for account 2 email for account 3… enqueues SCHEDULING EXECUTING generates
  39. ! @kwugirl cron job enqueues email for account 1 email

    for account 2 email for account 3… generates WeeklyEmailJob for account 1 WeeklyEmailJob for account 2 WeeklyEmailJob for account 3…
  40. ! @kwugirl cron job enqueues Monday 10am Pacific time email

    for account 1 email for account 2 email for account 3… generates WeeklyEmailJob for account 1 WeeklyEmailJob for account 2 WeeklyEmailJob for account 3…
  41. ! @kwugirl

  42. ! @kwugirl however…

  43. ! @kwugirl Monday 10am Pacific time == Tuesday 2am Tokyo

    Monday Tuesday
  44. ! @kwugirl MOAR ACCOUNTS

  45. ! @kwugirl MOAR ACCOUNTS

  46. ! @kwugirl “Monday” report 
 received on Tuesday! Sun Mon

    Tues Wed Thurs Fri Sat
  47. ! @kwugirl the proposed solution

  48. ! @kwugirl set up report jobs to run Monday 1:01am

    local time Local time Pacific time WeeklyEmailJob for account 1 Mon 1:01am JST Sun 9:01am WeeklyEmailJob for account 2 Mon 1:01am EDT Sun 10:01pm WeeklyEmailJob for account 3 Mon 1:01am HAST Mon 4:01am
  49. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… enqueues email for account 1 email for account 2 email for account 3… generates local
  50. ! @kwugirl cron job WeeklyEmailJob for account 1 WeeklyEmailJob for

    account 2 WeeklyEmailJob for account 3… enqueues Saturday 10am Pacific time email for account 1 email for account 2 email for account 3… generates local
  51. ! @kwugirl report processing spread out 0 25 50 75

    100 8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT before
  52. ! @kwugirl report processing spread out 0 25 50 75

    100 8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT before 0 25 50 75 100 8am PT 9am PT 10am PT 11am PT 12pm PT 1pm PT 2pm PT 3pm PT after
  53. ! @kwugirl first attempt

  54. ! @kwugirl what date is the next Monday?

  55. ! @kwugirl

  56. ! @kwugirl [1] pry(main)> Date.today

  57. ! @kwugirl [1] pry(main)> Date.today => Sun, 05 Apr 2015

  58. ! @kwugirl [1] pry(main)> Date.today => Sun, 05 Apr 2015

    [2] pry(main)> DateTime.parse("Monday, 1:01")
  59. ! @kwugirl [1] pry(main)> Date.today => Sun, 05 Apr 2015

    [2] pry(main)> DateTime.parse("Monday, 1:01") => Mon, 06 Apr 2015 01:01:00 +0000
  60. ! @kwugirl how do I get that into 
 the

    local time zone?
  61. ! @kwugirl Rails Time/Date classes

  62. ! @kwugirl TZInfo daylight saving-aware time conversions

  63. ! @kwugirl Internet Assigned Numbers Authority (IANA) Time Zone Database

  64. ! @kwugirl April 2015 May 2012

  65. ! @kwugirl ActiveRecord pins TZInfo version

  66. ! @kwugirl ActiveRecord pins TZInfo version (need to upgrade Rails

    versions to get accurate data)
  67. ! @kwugirl ActiveSupport::TimeZone

  68. ! @kwugirl ActiveSupport::TimeZone TZInfo::Timezone instances

  69. ! @kwugirl “a meaningful subset of 
 146 time zones”

    
 (out of many more)
  70. ! @kwugirl ~24 bands vs…

  71. ! @kwugirl { "International Date Line West" => "Pacific/Midway", "Midway

    Island" => "Pacific/Midway", "American Samoa" => "Pacific/Pago_Pago", "Hawaii" => "Pacific/Honolulu", "Alaska" => "America/Juneau", "Pacific Time (US & Canada)" => "America/Los_Angeles", "Tijuana" => "America/Tijuana", "Mountain Time (US & Canada)" => "America/Denver", "Arizona" => "America/Phoenix", "Chihuahua" => "America/Chihuahua", "Mazatlan" => "America/Mazatlan", "Central Time (US & Canada)" => "America/Chicago", "Saskatchewan" => "America/Regina", "Guadalajara" => "America/Mexico_City", "Mexico City" => "America/Mexico_City", "Monterrey" => "America/Monterrey", "Central America" => "America/Guatemala", "Eastern Time (US & Canada)" => "America/New_York", "Indiana (East)" => "America/Indiana/Indianapolis", "Bogota" => "America/Bogota", "Lima" => "America/Lima", "Quito" => "America/Lima", "Atlantic Time (Canada)" => "America/Halifax", "Caracas" => "America/Caracas", "La Paz" => "America/La_Paz", "Santiago" => "America/Santiago", "Newfoundland" => "America/St_Johns", "Brasilia" => "America/Sao_Paulo", "Buenos Aires" => "America/Argentina/Buenos_Aires", "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", "Azores" => "Atlantic/Azores", "Cape Verde Is." => "Atlantic/Cape_Verde", "Dublin" => "Europe/ Dublin", "Edinburgh" => "Europe/London", "Lisbon" => "Europe/Lisbon", "London" => "Europe/London", "Casablanca" => "Africa/Casablanca", "Monrovia" => "Africa/Monrovia", "UTC" => "Etc/UTC", "Belgrade" => "Europe/Belgrade", "Bratislava" => "Europe/Bratislava", "Budapest" => "Europe/Budapest", "Ljubljana" => "Europe/Ljubljana", "Prague" => "Europe/Prague", "Sarajevo" => "Europe/Sarajevo", "Skopje" => "Europe/Skopje", "Warsaw" => "Europe/Warsaw", "Zagreb" => "Europe/Zagreb", "Brussels" => "Europe/Brussels", "Copenhagen" => "Europe/Copenhagen", "Madrid" => "Europe/Madrid", "Paris" => "Europe/Paris", "Amsterdam" => "Europe/Amsterdam", "Berlin" => "Europe/Berlin", "Bern" => "Europe/ Berlin", "Rome" => "Europe/Rome", "Stockholm" => "Europe/Stockholm", "Vienna" => "Europe/Vienna", "West Central Africa" => "Africa/Algiers", "Bucharest" => "Europe/Bucharest", "Cairo" => "Africa/Cairo", "Helsinki" => "Europe/ Helsinki", "Kyiv" => "Europe/Kiev", "Riga" => "Europe/Riga", "Sofia" => "Europe/Sofia", "Tallinn" => "Europe/Tallinn", "Vilnius" => "Europe/Vilnius", "Athens" => "Europe/Athens", "Istanbul" => "Europe/Istanbul", "Minsk" => "Europe/ Minsk", "Jerusalem" => "Asia/Jerusalem", "Harare" => "Africa/Harare", "Pretoria" => "Africa/Johannesburg", "Kaliningrad" => "Europe/Kaliningrad", "Moscow" => "Europe/Moscow", "St. Petersburg" => "Europe/Moscow", "Volgograd" => "Europe/Volgograd", "Samara" => "Europe/Samara", "Kuwait" => "Asia/Kuwait", "Riyadh" => "Asia/Riyadh", "Nairobi" => "Africa/Nairobi", "Baghdad" => "Asia/Baghdad", "Tehran" => "Asia/Tehran", "Abu Dhabi" => "Asia/Muscat", "Muscat" => "Asia/Muscat", "Baku" => "Asia/Baku", "Tbilisi" => "Asia/Tbilisi", "Yerevan" => "Asia/Yerevan", "Kabul" => "Asia/Kabul", "Ekaterinburg" => "Asia/Yekaterinburg", "Islamabad" => "Asia/Karachi", "Karachi" => "Asia/Karachi", "Tashkent" => "Asia/Tashkent", "Chennai" => "Asia/Kolkata", "Kolkata" => "Asia/Kolkata", "Mumbai" => "Asia/Kolkata", "New Delhi" => "Asia/Kolkata", "Kathmandu" => "Asia/Kathmandu", "Astana" => "Asia/Dhaka", "Dhaka" => "Asia/Dhaka", "Sri Jayawardenepura" => "Asia/Colombo", "Almaty" => "Asia/Almaty", "Novosibirsk" => "Asia/Novosibirsk", "Rangoon" => "Asia/Rangoon", "Bangkok" => "Asia/Bangkok", "Hanoi" => "Asia/Bangkok", "Jakarta" => "Asia/Jakarta", "Krasnoyarsk" => "Asia/Krasnoyarsk", "Beijing" => "Asia/Shanghai", "Chongqing" => "Asia/Chongqing", "Hong Kong" => "Asia/Hong_Kong", "Urumqi" => "Asia/Urumqi", "Kuala Lumpur" => "Asia/Kuala_Lumpur", "Singapore" => "Asia/Singapore", "Taipei" => "Asia/ Taipei", "Perth" => "Australia/Perth", "Irkutsk" => "Asia/Irkutsk", "Ulaanbaatar" => "Asia/Ulaanbaatar", "Seoul" => "Asia/Seoul", "Osaka" => "Asia/Tokyo", "Sapporo" => "Asia/Tokyo", "Tokyo" => "Asia/Tokyo", "Yakutsk" => "Asia/ Yakutsk", "Darwin" => "Australia/Darwin", "Adelaide" => "Australia/Adelaide", "Canberra" => "Australia/Melbourne", "Melbourne" => "Australia/Melbourne", "Sydney" => "Australia/Sydney", "Brisbane" => "Australia/Brisbane", "Hobart" => "Australia/Hobart", "Vladivostok" => "Asia/Vladivostok", "Guam" => "Pacific/Guam", "Port Moresby" => "Pacific/Port_Moresby", "Magadan" => "Asia/Magadan", "Srednekolymsk" => "Asia/Srednekolymsk", "Solomon Is." => "Pacific/ Guadalcanal", "New Caledonia" => "Pacific/Noumea", "Fiji" => "Pacific/Fiji", "Kamchatka" => "Asia/Kamchatka", "Marshall Is." => "Pacific/Majuro", "Auckland" => "Pacific/Auckland", "Wellington" => "Pacific/Auckland", "Nuku'alofa" => "Pacific/Tongatapu", "Tokelau Is." => "Pacific/Fakaofo", "Chatham Is." => "Pacific/Chatham", "Samoa" => "Pacific/Apia" } ~24 bands vs…
  72. ! @kwugirl friendlier zone names “Eastern Time (US & Canada)”

    vs “America/New_York”
  73. ! @kwugirl getting time zone names

  74. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact!
  75. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact! => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"]
  76. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact! => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"] [2] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name if z.formatted_offset == "-11:00"}.compact!
  77. ! @kwugirl getting time zone names [1] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name

    if z.formatted_offset == "+12:00"}.compact! => ["Fiji", "Kamchatka", "Marshall Is.", "Auckland", “Wellington"] [2] pry(main)> ActiveSupport::TimeZone.zones_map.values.collect{|z| z.name if z.formatted_offset == "-11:00"}.compact! => ["International Date Line West", "Midway Island", "American Samoa"]
  78. ! @kwugirl (no UTC -12:00 offset)

  79. ! @kwugirl (no UTC -12:00 offset) “The Baker and Howland

    islands are not there. 
 But as (wikipedia says) they are uninhabited, they would be a quite specific use case. 
 I don't think they're going to be added to TZInfo.” — dmathieu
 https://github.com/rails/rails/issues/11390
  80. ! @kwugirl how do I get a DateTime 
 into

    the local time zone?
  81. ! @kwugirl ActiveSupport::TimeZone ! local_to_utc(time, dst=true) Adjust the given time

    to the simultaneous time in UTC. Returns a Time.utc() instance. !
  82. ! @kwugirl attempt #1’s code

  83. ! @kwugirl attempt #1’s code def local_1am(account_time_zone)

  84. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”)
  85. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”) default = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
  86. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”) default = ActiveSupport::TimeZone['Pacific Time (US & Canada)'] local_time_zone = account_time_zone || default

  87. ! @kwugirl attempt #1’s code def local_1am(account_time_zone) dt = DateTime.parse("Monday,

    1:01”) default = ActiveSupport::TimeZone['Pacific Time (US & Canada)'] local_time_zone = account_time_zone || default
 local_time_zone.local_to_utc(dt) end
  88. ! @kwugirl attempt #1’s tests

  89. ! @kwugirl

  90. ! @kwugirl

  91. ! @kwugirl context "enqueue time" do

  92. ! @kwugirl context "enqueue time" do setup do
 saturday_pt =

    DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")
 Timecop.freeze(saturday_pt)
 end

  93. ! @kwugirl context "enqueue time" do setup do
 saturday_pt =

    DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 # your tests

  94. ! @kwugirl context "enqueue time" do setup do
 saturday_pt =

    DateTime.parse("4th Apr 2015 10:05:00 AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 # your tests
 teardown do
 Timecop.return
 end
 end
  95. ! @kwugirl # Running: ! .. ! Finished in -37452054.639617s,

    -0.0000 runs/s, -0.0000 assertions/s. ! 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  96. ! @kwugirl # Running: ! .. ! Finished in -37452054.639617s,

    -0.0000 runs/s, -0.0000 assertions/s. ! 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  97. ! @kwugirl wat

  98. ! @kwugirl

  99. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end

  100. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do
  101. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"]
  102. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00")
  103. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") assert_equal expected, local_1am(mtn_tz) end
  104. ! @kwugirl UTC Mountain time Bangkok time

  105. ! @kwugirl # Running: ! .. ! Finished in 0.059444s,

    33.6451 runs/s, 33.6451 assertions/s ! 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
  106. ! @kwugirl scheduling changes

  107. ! @kwugirl before cron job enqueues Monday 10am Pacific time

    email for account 1 email for account 2 email for account 3… generates WeeklyEmailJob for account 1 WeeklyEmailJob for account 2 WeeklyEmailJob for account 3…
  108. ! @kwugirl after cron job WeeklyEmailJob for account 1 WeeklyEmailJob

    for account 2 WeeklyEmailJob for account 3… enqueues Saturday 10am Pacific time email for account 1 email for account 2 email for account 3… generates local
  109. ! @kwugirl start on Sat morning if pdt_day == SATURDAY

    base_email = 'script/run_weekly_email_report.rb' Async::Command::Bulk.new(base_email).enqueue end
  110. ! @kwugirl queue up jobs with a time WeeklyEmailReportJob.new(account.id).enqueue_at(local_1am(tz))

  111. ! @kwugirl

  112. ! @kwugirl FAILURE

  113. ! @kwugirl had to manually regenerate 
 all email reports

  114. ! @kwugirl Lesson #2: Have a backup plan

  115. ! @kwugirl unknown unknowns

  116. ! @kwugirl

  117. ! @kwugirl Lesson #3: Do an internal test run

  118. ! @kwugirl so what happened?

  119. ! @kwugirl that week’s emails were queued for 1:01am local

    time…last Monday
  120. ! @kwugirl can’t tell whether it’s closest date or last

    date Sun Mon Tues Wed Thurs Fri Sat
  121. ! @kwugirl [1] pry(main)> Date.today can’t tell whether it’s closest

    date or last date Sun Mon Tues Wed Thurs Fri Sat
  122. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  123. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  124. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  125. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  126. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  127. ! @kwugirl [1] pry(main)> Date.today => Thu, 09 Apr 2015

    [2] pry(main)> DateTime.parse("Monday 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 can’t tell whether it’s closest date or last date Sun Mon Tues Wed Thurs Fri Sat
  128. ! @kwugirl Lesson #4: Today’s console results are not tomorrow’s

    console results
  129. ! @kwugirl but…tests??

  130. ! @kwugirl DateTime.parse not friends with Timecop

  131. ! @kwugirl changed test setup

  132. ! @kwugirl changed test setup saturday_pt = DateTime.parse("1st May 2015

    10:05:00 AM -07:00")
  133. ! @kwugirl changed test setup saturday_pt = DateTime.parse("1st May 2015

    10:05:00 AM -07:00") Timecop.freeze(saturday_pt)
  134. ! @kwugirl changed test setup saturday_pt = DateTime.parse("1st May 2015

    10:05:00 AM -07:00") Timecop.freeze(saturday_pt) setting date to May 1st (Fri)! want to get May 4th (next Mon)
  135. ! @kwugirl

  136. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now

  137. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


  138. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    date set to May 1st
  139. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    [2] pry(#<SchedulingHelperTest>)> DateTime.parse("Monday, 1:01") date set to May 1st
  140. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    [2] pry(#<SchedulingHelperTest>)> DateTime.parse("Monday, 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 date set to May 1st
  141. ! @kwugirl [1] pry(#<SchedulingHelperTest>)> Time.now => 2015-05-01 10:05:00 -0700
 


    [2] pry(#<SchedulingHelperTest>)> DateTime.parse("Monday, 1:01") => Mon, 06 Apr 2015 01:01:00 +0000 date set to May 1st DateTime parses to Apr 6th?!?
  142. ! @kwugirl

  143. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  144. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  145. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end Test only passed because happened to run in the same week of Apr 4th, not because of Timecop.freeze
  146. ! @kwugirl

  147. ! @kwugirl

  148. ! @kwugirl

  149. ! @kwugirl Lesson #5: Guard against writing false positive tests

  150. ! @kwugirl actual solution

  151. ! @kwugirl TDD, that’s a thing

  152. ! @kwugirl setup do
 saturday_pt = DateTime.parse("4th Apr 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("6th Apr 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  153. ! @kwugirl setup do
 saturday_pt = DateTime.parse("8th Feb 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("10th Feb 2015 01:01:00 AM -06:00") ! assert_equal expected, local_1am(mtn_tz) end
  154. ! @kwugirl setup do
 saturday_pt = DateTime.parse("8th Feb 2015 10:05:00

    AM -07:00")
 Timecop.freeze(saturday_pt)
 end
 should "be the next Monday 1:01am local time (west of UTC)" do mtn_tz = ActiveSupport::TimeZone["Mountain Time (US & Canada)"] expected = DateTime.parse("10th Feb 2015 01:01:00 AM -06:00") assert_wday(expected, "Monday") ! assert_equal expected, local_1am(mtn_tz) end
  155. ! @kwugirl # Running: ! FF ! Finished in 0.061686s,

    32.4223 runs/s, 64.8445 assertions/s. ! 1) Failure: SchedulingHelperTest#test_: enqueue time should be the next Monday 1:01am local time (west of UTC). -Mon, 10 Feb 2015 01:01:00 -0600 +Mon, 06 Apr 2015 07:01:00 +0000 ! 2) Failure: SchedulingHelperTest#test_: enqueue time should be the next Monday 1:01am local time (east of UTC). -Mon, 10 Feb 2015 01:01:00 +0700 +Sun, 05 Apr 2015 18:01:00 +0000 ! ! 2 runs, 4 assertions, 2 failures, 0 errors, 0 skips
  156. ! @kwugirl need another way to get “next Monday” 


    besides simple DateTime.parse
  157. ! @kwugirl Lesson #2: Have a backup plan

  158. ! @kwugirl might need to run script after Saturday, if

    Saturday job fails
  159. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  160. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  161. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  162. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  163. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  164. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  165. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  166. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  167. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  168. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  169. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  170. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  171. ! @kwugirl distinguishing between “this” week and “next” week?

  172. ! @kwugirl multiple time zones

  173. ! @kwugirl multiple time zones • the time we’re used

    to thinking of (Pacific time)
  174. ! @kwugirl multiple time zones • the time we’re used

    to thinking of (Pacific time) • server’s time zone (who even knows)
  175. ! @kwugirl multiple time zones • the time we’re used

    to thinking of (Pacific time) • server’s time zone (who even knows) • the customer’s time zone (could be ahead of us or behind us when running script)
  176. ! @kwugirl need logic to apply globally

  177. ! @kwugirl if it’s still Monday somewhere, anywhere, we’ll think

    of it as “this” week still
  178. ! @kwugirl once it’s at least Tuesday everywhere, script should

    schedule reports for the next Monday
  179. ! @kwugirl Lesson #6: Figure out a consistent, global logic

  180. ! @kwugirl

  181. ! @kwugirl def local_1am(account_time_zone)

  182. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

  183. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7
  184. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday

  185. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time']
  186. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.formatted_offset
  187. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.formatted_offset like “+07:00”
  188. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.formatted_offset 
 DateTime.parse("#{next_monday} 01:01:00 AM #{current_tz_offset}") end
  189. ! @kwugirl # Running: ! .. ! Finished in 0.041562s,

    48.1209 runs/s, 96.2418 assertions/s. ! 2 runs, 4 assertions, 0 failures, 0 errors, 0 skips
  190. ! @kwugirl MOAR TESTS

  191. ! @kwugirl context "enqueue time" do context "during U.S. standard

    time" do… end ! context "during U.S. daylight saving" do… end
  192. ! @kwugirl # Running: ! ..F. ! Finished in 0.061256s,

    65.2997 runs/s, 130.5995 assertions/s. ! 1) Failure: SchedulingHelperTest#test_: enqueue time during U.S. daylight saving should be the next Monday 1:01am local time (west of UTC). --- expected +++ actual @@ -1 +1 @@ -Mon, 09 Jun 2014 01:01:00 -0600 +Mon, 09 Jun 2014 01:01:00 -0700 ! ! 4 runs, 8 assertions, 1 failures, 0 errors, 0 skips
  193. ! @kwugirl # Running: ! ..F. ! Finished in 0.061256s,

    65.2997 runs/s, 130.5995 assertions/s. ! 1) Failure: SchedulingHelperTest#test_: enqueue time during U.S. daylight saving should be the next Monday 1:01am local time (west of UTC). --- expected +++ actual @@ -1 +1 @@ -Mon, 09 Jun 2014 01:01:00 -0600 +Mon, 09 Jun 2014 01:01:00 -0700 ! ! 4 runs, 8 assertions, 1 failures, 0 errors, 0 skips off by 1…
  194. ! @kwugirl def local_1am(account_time_zone) last_day = ActiveSupport::TimeZone['International Date Line West'].today

    days_until_monday = (8 - last_day.wday) % 7 next_monday = last_day + days_until_monday
 local_tz = account_time_zone || ActiveSupport::TimeZone['Pacific Time'] current_tz_offset = local_timezone.now.formatted_offset 
 DateTime.parse("#{next_monday} 01:01:00 AM #{current_tz_offset}") end
  195. ! @kwugirl # Running: ! .... ! Finished in 0.043776s,

    91.3743 runs/s, 182.7485 assertions/s. ! 4 runs, 8 assertions, 0 failures, 0 errors, 0 skips
  196. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  197. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  198. ! @kwugirl context "when run on different days of the

    week" do setup do @samoa_tz = ActiveSupport::TimeZone[“American Samoa"] @wellington_tz = ActiveSupport::TimeZone["Wellington"] end
  199. ! @kwugirl

  200. ! @kwugirl should "be the next day when run on

    late Sunday night" do
  201. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday")
  202. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday")
  203. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday") # It's already Monday in the customer account's time zone
 assert_wday(@wellington_tz.now, "Monday")

  204. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday") # It's already Monday in the customer account's time zone
 assert_wday(@wellington_tz.now, "Monday")
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")

  205. ! @kwugirl should "be the next day when run on

    late Sunday night" do sunday_night = DateTime.parse("1st Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(sunday_night)
 assert_wday(@samoa_tz.now, "Sunday") # It's already Monday in the customer account's time zone
 assert_wday(@wellington_tz.now, "Monday")
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")
 assert_equal expected, local_1am(local_tz)
 end
  206. ! @kwugirl # Running: ! ..... ! Finished in 0.059667s,

    83.7984 runs/s, 201.1162 assertions/s. ! 5 runs, 12 assertions, 0 failures, 0 errors, 0 skips
  207. ! @kwugirl

  208. ! @kwugirl should "be the same week when still Monday

    somewhere" do
  209. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday")
  210. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

  211. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday")
  212. ! @kwugirl should "be the same week when still Monday

    somewhere" do monday = DateTime.parse("2nd Jun 2014 11:30:00 PM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(monday)
 assert_wday(@samoa_tz.now, "Monday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("2nd Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday") 
 assert_equal expected, local_1am(local_tz) end
  213. ! @kwugirl # Running: ! ...... ! Finished in 0.059173s,

    101.3976 runs/s, 253.4940 assertions/s. ! 6 runs, 15 assertions, 0 failures, 0 errors, 0 skips
  214. ! @kwugirl Sun Mon Tues Wed Thurs Fri Sat

  215. ! @kwugirl

  216. ! @kwugirl should "be the next week when run on

    Tuesday" do
  217. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday")
  218. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]

  219. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("9th Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday")

  220. ! @kwugirl should "be the next week when run on

    Tuesday" do tuesday = DateTime.parse("3rd Jun 2014 00:30:00 AM #{@samoa_tz.formatted_offset}")
 Timecop.freeze(tuesday)
 assert_wday(tuesday, "Tuesday") 
 local_tz = ActiveSupport::TimeZone[@wellington_tz.name]
 expected = DateTime.parse("9th Jun 2014 01:01:00 AM +12:00")
 assert_wday(expected, "Monday")
 assert_equal expected, local_1am(local_tz) end
  221. ! @kwugirl # Running: ! ....... ! Finished in 0.060848s,

    115.0408 runs/s, 295.8191 assertions/s. ! 7 runs, 18 assertions, 0 failures, 0 errors, 0 skips
  222. ! @kwugirl tests — Before • context "enqueue time" •

    should "be the next Monday 1:01am local time (west of UTC)" • should "be the next Monday 1:01am local time (east of UTC)"
  223. ! @kwugirl tests — After • context "enqueue time" •

    context "during U.S. standard time” • should "be the next Monday 1:01am local time (west of UTC)" • should "be the next Monday 1:01am local time (east of UTC)" • context "during U.S. daylight saving” • should "be the next Monday 1:01am local time (west of UTC)" • should "be the next Monday 1:01am local time (east of UTC)” • context "when run on different days of the week" • should "be the next day when run on late Sunday night" • should "be the same week when still Monday somewhere" • should "be the next week when run on Tuesday"
  224. ! @kwugirl tests to write • covering time zones west/east

    of UTC • pin down exactly when you’re starting and when you’re ending • pick random dates in the past or in the future--not current dates • test dates inside/outside daylight saving period • check that the dates being used in your tests are the expected day of the week • test when triggering the scripts on different days of the week • test on dates of daylight saving transitions
  225. ! @kwugirl scheduling process changes

  226. ! @kwugirl Lesson #2: Have a backup plan

  227. ! @kwugirl keep option to trigger manually if @options[:enqueue_later] WeeklyEmailJob.new(id).enqueue_at(local_1am(tz))

    else WeeklyEmailJob.new(id).enqueue end
  228. ! @kwugirl Lesson #3: Do an internal test run

  229. ! @kwugirl

  230. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb"
  231. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb" 
 options = " --account 3,10 --enqueue_later"
  232. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb" 
 options = " --account 3,10 --enqueue_later" recipient = " --to \"kwu+emailtest@newrelic.com\""
  233. ! @kwugirl # Test for enqueueing weekly emails jobs on

    Saturday if pdt_day == SATURDAY base = "script/run_weekly_email_report.rb" 
 options = " --account 3,10 --enqueue_later" recipient = " --to \"kwu+emailtest@newrelic.com\"" 
 test_email = [base, options, recipient].join("") Async::Command::Bulk.new(test_email).enqueue end
  234. ! @kwugirl

  235. ! @kwugirl

  236. ! @kwugirl

  237. ! @kwugirl lessons 1. Trust NOTHING! 2. Have a backup

    plan! 3. Do an internal test run! 4. Today’s console results are not 
 tomorrow’s console results ! 5. Guard against writing false positive tests! 6. Figure out a consistent, global logic