canvas-lms/config/initializers/switchman.rb

247 lines
8.0 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2013 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
Rails.application.config.after_initialize do
# WillPaginate needs to allow args to Relation#to_a
WillPaginate::ActiveRecord::RelationMethods.class_eval do
def to_a(*args)
if current_page.nil? then super # workaround for Active Record 3.0
else
WillPaginate::Collection.create(current_page, limit_value) do |col|
col.replace super
col.next_page = nil if total_entries.nil? && col.respond_to?(:length) && col.length < col.per_page # don't return a next page if there's nothing to get next
col.total_entries ||= total_entries
end
end
end
end
module Canvas # rubocop:disable Lint/ConstantDefinitionInBlock
module Shard
module IncludedClassMethods
def birth
default
end
end
def settings
return {} unless self.class.columns_hash.key?("settings")
s = super
# Am not sure how this happens
if s.is_a?(String)
s = JSON.parse(s)
end
if s.nil?
self.settings = s = {}
end
salt = s.delete(:encryption_key_salt)
secret = s.delete(:encryption_key_enc)
if secret || salt
if secret && salt
s[:encryption_key] = Canvas::Security.decrypt_password(secret, salt, "shard_encryption_key")
end
self.settings = s
end
s
end
def encrypt_settings
s = settings.dup
if (encryption_key = s.delete(:encryption_key))
secret, salt = Canvas::Security.encrypt_password(encryption_key, "shard_encryption_key")
s[:encryption_key_enc] = secret
s[:encryption_key_salt] = salt
end
if s != settings
self.settings = s
end
s
end
end
module DisableActivateBang
if ::Rails.env.test?
def activate!(*)
raise NotImplementedError # if you're getting this, you really should be using activate instead of activate!
end
end
end
end
Switchman::Shard.prepend(Canvas::Shard)
Switchman::Shard.singleton_class.include(Canvas::Shard::IncludedClassMethods)
Switchman::Shard.prepend(Canvas::DisableActivateBang)
Switchman::DefaultShard.prepend(Canvas::DisableActivateBang)
Switchman::Shard.class_eval do
self.primary_key = "id"
reset_column_information if connected? # make sure that the id column object knows it is the primary key
before_save :encrypt_settings
end
Switchman::DatabaseServer.class_eval do
def next_maintenance_window
return nil unless maintenance_window_start_hour
now = Time.now.utc
date = nil
weekday = maintenance_window_weekday
weeks = maintenance_window_weeks_of_month
loop do
first_date = first_given_weekday_of_month(weekday, now)
# look at the 1st, 3rd, etc. weekday of this month, and see if it's in the future
weeks.each do |i|
new_date = first_date.advance(weeks: i - 1)
# make sure we didn't overrun the current month, like if there's not a 5th thursday this month
if new_date.future? && new_date.month == now.month
date = new_date
break
end
end
break if date
# search the next month
now = now.next_month
end
# Time offsets are strange
start_at = date.beginning_of_day - maintenance_window_start_hour.hours + maintenance_window_offset.minutes
end_at = start_at + maintenance_window_duration
[start_at, end_at]
end
# Finds the first day of the month that is a given weekday
#
# @param [Integer] which weekday we're looking for
# @param [Time] start_ref the month to look in
def first_given_weekday_of_month(weekday, start_ref)
date = start_ref.beginning_of_month
date = date.next_day until date.wday == weekday
date
end
def maintenance_window_start_hour
Setting.get("maintenance_window_start_hour", nil)&.to_i
end
def maintenance_window_offset
Setting.get("maintenance_window_offset", "0").to_i
end
def maintenance_window_duration
# ISO 8601 duration
ActiveSupport::Duration.parse(Setting.get("maintenance_window_duration", "PT2H"))
end
# @return [Integer] the weekday of the maintenance window
def maintenance_window_weekday
Date::DAYNAMES.index(Setting.get("maintenance_window_weekday", "thursday").capitalize)
end
# @return [Array<Integer>]
# the weeks of the month that the maintenance window occurs on, sorted and 1-indexed
def maintenance_window_weeks_of_month
Setting.get("maintenance_window_weeks_of_month", "1,3").split(",").map(&:to_i).sort
end
def self.send_in_each_region(klass, method, enqueue_args, *args, **kwargs)
run_current_region_asynchronously = enqueue_args.delete(:run_current_region_asynchronously)
return klass.send(method, *args, **kwargs) if DatabaseServer.all.all? { |db| !db.config[:region] }
regions = Set.new
unless run_current_region_asynchronously
klass.send(method, *args, **kwargs)
regions << Shard.current.database_server.config[:region]
end
all.each do |db|
next if regions.include?(db.config[:region]) || !db.config[:region]
next if db.shards.empty?
regions << db.config[:region]
db.shards.first.activate do
klass.delay(**enqueue_args).__send__(method, *args, **kwargs)
end
end
end
def self.send_in_region(region, klass, method, enqueue_args, *args, **kwargs)
return klass.delay(**enqueue_args).__send__(method, *args, **kwargs) if region.nil?
shard = nil
all.find { |db| db.config[:region] == region && (shard = db.shards.first) }
# the app server knows what region it's in, but the database servers don't?
# just send locally
if shard.nil? && all.all? { |db| db.config[:region].nil? }
return klass.delay(**enqueue_args).__send__(method, *args, **kwargs)
end
raise "Could not find a shard in region #{region}" unless shard
shard.activate do
klass.delay(**enqueue_args).__send__(method, *args, **kwargs)
end
end
end
Object.send(:remove_const, :Shard) if defined?(Shard)
Object.send(:remove_const, :DatabaseServer) if defined?(DatabaseServer)
# rubocop:disable Lint/ConstantDefinitionInBlock
Shard = Switchman::Shard
DatabaseServer = Switchman::DatabaseServer
# rubocop:enable Lint/ConstantDefinitionInBlock
Switchman::DefaultShard.class_eval do
attr_writer :settings
def settings
{}
end
end
if !Shard.default.is_a?(Shard) && Switchman.config[:force_sharding] && !ENV["SKIP_FORCE_SHARDING"]
raise "Sharding is supposed to be set up, but is not! Use SKIP_FORCE_SHARDING=1 to ignore"
end
if Shard.default.is_a?(Shard)
# otherwise the serialized settings attribute method won't be properly defined
Shard.define_attribute_methods
Shard.default.instance_variable_set(:@attributes, Shard.attributes_builder.build_from_database(Shard.default.attributes_before_type_cast))
end
Switchman::Deprecation.behavior = [
:log,
lambda do |message, callstack, _deprecation_horizon, _gem_name|
e = ActiveSupport::DeprecationException.new(message)
e.set_backtrace(callstack.map(&:to_s))
Sentry.capture_exception(e, level: :warning)
end
]
Switchman.config[:region] = Canvas.region
end