# 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 . 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] # 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