Show a maintenance window

Per-database-server offsets will be added in a separate commit

refs FOO-1600

Change-Id: I836691ba18b373717f139e116b9b91376f67b682
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/260356
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Ethan Vizitei <evizitei@instructure.com>
QA-Review: Jacob Burroughs <jburroughs@instructure.com>
Product-Review: Jacob Burroughs <jburroughs@instructure.com>
This commit is contained in:
Jacob Burroughs 2021-03-10 11:28:43 -06:00
parent 208407d180
commit 16b1a67f8e
9 changed files with 135 additions and 8 deletions

View File

@ -128,6 +128,8 @@ gem 'twilio-ruby', '5.36.0', require: false
gem 'tzinfo', '1.2.7'
gem 'vault', '0.15.0', require: false
gem 'vericite_api', '1.5.3'
gem 'week_of_month', '1.2.5',
github: 'instructure/week-of-month', ref: 'b3013639e9474f302b5a6f27e4e45313e8d24902'
gem 'will_paginate', '3.3.0', require: false # required for folio-pagination
path 'engines' do

View File

@ -134,6 +134,12 @@
<%= f.time_zone_select :default_time_zone, I18nTimeZone.us_zones, :model => I18nTimeZone %>
</td>
</tr>
<% if @account.shard.database_server.maintenance_window_start_hour %>
<tr>
<td></td>
<td> <%= render partial: 'shared/maintenance_window', locals: { database_server: @account.shard.database_server } %> </td>
</tr>
<% end %>
<%= f.fields_for :settings do |settings| %>
<% if @account.grants_right?(@current_user, :manage_site_settings) %>
<tr>

View File

@ -157,6 +157,12 @@
<span class="edit_data"><%= f.time_zone_select :time_zone, I18nTimeZone.us_zones, :model => I18nTimeZone, :default => (@domain_root_account.try(:default_time_zone) || "Mountain Time (US & Canada)") %></span>
</td>
</tr>
<% if @user.shard.database_server.maintenance_window_start_hour %>
<tr>
<td></td>
<td> <%= render partial: 'shared/maintenance_window', locals: { database_server: @user.shard.database_server } %> </td>
</tr>
<% end %>
<% if @domain_root_account == Account.default %>
<tr>
<td colspan="2">

View File

@ -0,0 +1,9 @@
<% next_window = database_server.next_maintenance_window %>
<%= t("Maintenance windows: %{days} %{weekday} of the month from %{time_range} (%{utc_time_range} UTC )",
{ days: database_server.maintenance_window_weeks_of_month.map(&:ordinalize).to_sentence,
weekday: I18n.t('date.day_names')[next_window[0].wday],
time_range: time_string(next_window[0], next_window[1]),
utc_time_range: time_string(next_window[0], next_window[1], ActiveSupport::TimeZone['UTC']) }) %>
<br/>
<%= t("Next maintenance at: %{window}", {
window: datetime_string(next_window[0], :verbose, next_window[1], with_weekday: true) }) %>

View File

@ -142,6 +142,43 @@ Rails.application.config.after_initialize do
@in_current_region
end
def next_maintenance_window
return nil unless maintenance_window_start_hour
start_day = DateTime.now
# This array is effectively 1 indexed
relevant_weeks = maintenance_window_weeks_of_month.map { |i| WeekOfMonth::Constant::WEEKS_IN_SEQUENCE[i] }
maintenance_days = relevant_weeks.map do |ordinal|
Time.zone.local_to_utc(start_day.send("#{ordinal}_#{maintenance_window_weekday}_in_month".downcase))
end + relevant_weeks.map do |ordinal|
Time.zone.local_to_utc((start_day + 1.month).send("#{ordinal}_#{maintenance_window_weekday}_in_month".downcase))
end
next_day = maintenance_days.find { |d| d.future? }
# Time offsets are strange
start_at = next_day.utc.beginning_of_day - maintenance_window_start_hour.hours
end_at = start_at + maintenance_window_duration
[start_at, end_at]
end
def maintenance_window_start_hour
Setting.get('maintenance_window_start_hour', nil)&.to_i
end
def maintenance_window_duration
# ISO 8601 duration
ActiveSupport::Duration.parse(Setting.get('maintenance_window_duration', "PT2H"))
end
def maintenance_window_weekday
Setting.get('maintenance_window_weekday', 'thursday').downcase
end
def maintenance_window_weeks_of_month
Setting.get('maintenance_window_weeks_of_month', "1,3").split(',').map(&:to_i)
end
def self.send_in_each_region(klass, method, enqueue_args = {}, *args)
run_current_region_asynchronously = enqueue_args.delete(:run_current_region_asynchronously)

View File

@ -60,9 +60,9 @@ module TextHelper
end
end
def datetime_string(start_datetime, datetime_type=:event, end_datetime=nil, shorten_midnight=false, zone=nil)
def datetime_string(start_datetime, datetime_type=:event, end_datetime=nil, shorten_midnight=false, zone=nil, with_weekday: false)
zone ||= ::Time.zone
presenter = Utils::DatetimeRangePresenter.new(start_datetime, end_datetime, datetime_type, zone)
presenter = Utils::DatetimeRangePresenter.new(start_datetime, end_datetime, datetime_type, zone, with_weekday: with_weekday)
presenter.as_string(shorten_midnight: shorten_midnight)
end

View File

@ -18,13 +18,14 @@
module Utils
class DatePresenter
attr_reader :date, :raw_date, :zone
attr_reader :date, :raw_date, :zone, :with_weekday
def initialize(date, zone=nil)
def initialize(date, zone=nil, with_weekday: false)
zone ||= Time.zone
@raw_date = date
@date = RelativeDate.new(date, zone)
@zone = zone
@with_weekday = with_weekday
end
def as_string(style=:normal)
@ -52,7 +53,8 @@ module Utils
end
def i18n_date(format)
I18n.l(raw_date, format: I18n.send(:t, :"date.formats.#{format}"))
# Use send to prevent i18nliner trying to parse this
I18n.l(raw_date, format: I18n.send(:t, "date.formats.#{format}#{with_weekday ? '_with_weekday' : ''}"))
end
def special_value_type

View File

@ -18,13 +18,14 @@
module Utils
class DatetimeRangePresenter
attr_reader :start, :zone
def initialize(datetime, end_datetime = nil, datetime_type=:event, zone=nil)
attr_reader :start, :zone, :with_weekday
def initialize(datetime, end_datetime = nil, datetime_type=:event, zone=nil, with_weekday: false)
zone ||= ::Time.zone
@start = datetime.in_time_zone(zone) rescue datetime
@_finish = end_datetime.in_time_zone(zone) rescue end_datetime
@_datetime_type = datetime_type
@zone = zone
@with_weekday = with_weekday
end
def as_string(options={})
@ -94,7 +95,7 @@ module Utils
end
def present_date(date)
Utils::DatePresenter.new(date.to_date, zone).as_string(date_style)
Utils::DatePresenter.new(date.to_date, zone, with_weekday: with_weekday).as_string(date_style)
end
def finish

View File

@ -40,4 +40,68 @@ describe Switchman::Shard do
expect(Shard.in_region('eu-west-1')).to eq([s3])
end
end
describe "maintenance_windows" do
before do
allow(Setting).to receive(:get).and_call_original
end
it 'Returns an empty window if no start is defined' do
allow(Setting).to receive(:get).with("maintenance_window_start_hour", anything).and_return(nil)
expect(DatabaseServer.all.first.next_maintenance_window).to be(nil)
end
it 'Returns a window of the correct duration' do
allow(Setting).to receive(:get).with("maintenance_window_start_hour", anything).and_return('0')
allow(Setting).to receive(:get).with("maintenance_window_duration", anything).and_return('PT3H')
window = DatabaseServer.all.first.next_maintenance_window
expect(window[1] - window[0]).to eq(3.hours)
end
it 'Returns a window starting at the correct time' do
allow(Setting).to receive(:get).with("maintenance_window_start_hour", anything).and_return('3')
window = DatabaseServer.all.first.next_maintenance_window
expect(window[0].utc.hour).to eq(21)
allow(Setting).to receive(:get).with("maintenance_window_start_hour", anything).and_return('-7')
window = DatabaseServer.all.first.next_maintenance_window
expect(window[0].utc.hour).to eq(7)
end
it 'Returns a window on the correct day' do
allow(Setting).to receive(:get).with("maintenance_window_start_hour", anything).and_return('0')
allow(Setting).to receive(:get).with("maintenance_window_weekday", anything).and_return('Tuesday')
window = DatabaseServer.all.first.next_maintenance_window
expect(window[0].wday).to eq(Date::DAYNAMES.index('Tuesday'))
end
it 'Returns a window on the correct day of the month' do
allow(Setting).to receive(:get).with("maintenance_window_start_hour", anything).and_return('0')
allow(Setting).to receive(:get).with("maintenance_window_weekday", anything).and_return('Tuesday')
allow(Setting).to receive(:get).with("maintenance_window_weeks_of_month", anything).and_return('2,4')
Timecop.freeze(Time.utc(2021,3,1,12,0)) do
window = DatabaseServer.all.first.next_maintenance_window
# The 9th was the second tuesday of that month
expect(window[0].day).to eq(9)
end
Timecop.freeze(Time.utc(2021,3,10,12,0)) do
window = DatabaseServer.all.first.next_maintenance_window
# The 23rd was the fourth tuesday of that month
expect(window[0].day).to eq(23)
end
end
end
end