<commit_before>#
# Copyright (C) 2016 - 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/>.

# This is what is in charge of regenerating all of the child
# brand configs when an account saves theirs in the theme editor
class BrandConfigRegenerator

  attr_reader :progresses

  def initialize(account, current_user, new_brand_config)
    @account = account
    @current_user = current_user
    old_config_md5 = @account.brand_config_md5
    @account.brand_config = new_brand_config
    @account.save!
    @new_configs = {}
    @new_configs[old_config_md5] = new_brand_config if old_config_md5
    @progresses = []
    process
  end

  def things_that_need_to_be_regenerated
    @things_that_need_to_be_regenerated ||= begin
      all_subaccounts = @account.sub_accounts_recursive(100000, nil)
      result = all_subaccounts.select(&:brand_config_md5)
      result.concat(SharedBrandConfig.where(account_id: all_subaccounts))
      if @account.site_admin?
        # note: this is only root accounts on the same shard as site admin
        @account.shard.activate do
          root_scope = Account.root_accounts.active.where.not(id: @account)
          result.concat(root_scope.select(&:brand_config_md5))
          result.concat(SharedBrandConfig.where(account_id: root_scope))

          sub_scope = Account.active.where(root_account_id: root_scope)
          result.concat(sub_scope.select(&:brand_config_md5))
          result.concat(SharedBrandConfig.where(account_id: sub_scope))
        end
      end
      result
    end.freeze
  end

  # Returns true if this brand config is not based on anything that needs to be regenerated.
  # This should not be common but can happen in dev/test setups that got into an inconsistent state
  def orphan?(brand_config)
    things_that_need_to_be_regenerated.none? { |thing| thing.brand_config_md5 == brand_config.parent_md5 }
  end

  # If we haven't saved a new copy for a config's parent,
  # we don't know its new parent_md5 yet.
  def ready_to_process?(account_or_shared_brand_config)
    config = account_or_shared_brand_config.brand_config
    !config.parent || @new_configs.key?(config.parent_md5) || orphan?(config)
  end

  def regenerate(thing)
    config = thing.brand_config
    return unless config
    new_parent_md5 = config.parent_md5 && @new_configs[config.parent_md5].try(:md5) || @account.brand_config_md5
    new_config = config.clone_with_new_parent(new_parent_md5)
    new_config.save_unless_dup!

    account = thing.is_a?(SharedBrandConfig) ? thing.account : thing
    job_type = thing.is_a?(SharedBrandConfig) ? :sync_to_s3_and_save_to_shared_brand_config! : :sync_to_s3_and_save_to_account!
    progress = Progress.new(
      context: @current_user,
      tag: "#{job_type}_for_#{thing.id}".to_sym,
      message: "Syncing for #{account.name}#{thing.is_a?(SharedBrandConfig) ? ": #{thing.name}" : ''}"
    )
    progress.user = @current_user
    progress.reset!
    progress.process_job(new_config, job_type, {priority: Delayed::HIGH_PRIORITY}, thing.id)

    @new_configs[config.md5] = new_config
    @progresses << progress
  end

  def process
    things_left_to_process = things_that_need_to_be_regenerated.dup
    while thing = things_left_to_process.sample
      next unless ready_to_process?(thing)
      regenerate(thing)
      things_left_to_process.delete(thing)
    end
  end

end<commit_msg>strand brand config regeneration jobs<commit_after>#
# Copyright (C) 2016 - 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/>.

# This is what is in charge of regenerating all of the child
# brand configs when an account saves theirs in the theme editor
class BrandConfigRegenerator

  attr_reader :progresses

  def initialize(account, current_user, new_brand_config)
    @account = account
    @current_user = current_user
    old_config_md5 = @account.brand_config_md5
    @account.brand_config = new_brand_config
    @account.save!
    @new_configs = {}
    @new_configs[old_config_md5] = new_brand_config if old_config_md5
    @progresses = []
    process
  end

  def things_that_need_to_be_regenerated
    @things_that_need_to_be_regenerated ||= begin
      all_subaccounts = @account.sub_accounts_recursive(100000, nil)
      result = all_subaccounts.select(&:brand_config_md5)
      result.concat(SharedBrandConfig.where(account_id: all_subaccounts))
      if @account.site_admin?
        # note: this is only root accounts on the same shard as site admin
        @account.shard.activate do
          root_scope = Account.root_accounts.active.where.not(id: @account)
          result.concat(root_scope.select(&:brand_config_md5))
          result.concat(SharedBrandConfig.where(account_id: root_scope))

          sub_scope = Account.active.where(root_account_id: root_scope)
          result.concat(sub_scope.select(&:brand_config_md5))
          result.concat(SharedBrandConfig.where(account_id: sub_scope))
        end
      end
      result
    end.freeze
  end

  # Returns true if this brand config is not based on anything that needs to be regenerated.
  # This should not be common but can happen in dev/test setups that got into an inconsistent state
  def orphan?(brand_config)
    things_that_need_to_be_regenerated.none? { |thing| thing.brand_config_md5 == brand_config.parent_md5 }
  end

  # If we haven't saved a new copy for a config's parent,
  # we don't know its new parent_md5 yet.
  def ready_to_process?(account_or_shared_brand_config)
    config = account_or_shared_brand_config.brand_config
    !config.parent || @new_configs.key?(config.parent_md5) || orphan?(config)
  end

  def regenerate(thing)
    config = thing.brand_config
    return unless config
    new_parent_md5 = config.parent_md5 && @new_configs[config.parent_md5].try(:md5) || @account.brand_config_md5
    new_config = config.clone_with_new_parent(new_parent_md5)
    new_config.save_unless_dup!

    account = thing.is_a?(SharedBrandConfig) ? thing.account : thing
    job_type = thing.is_a?(SharedBrandConfig) ? :sync_to_s3_and_save_to_shared_brand_config! : :sync_to_s3_and_save_to_account!
    progress = Progress.new(
      context: @current_user,
      tag: "#{job_type}_for_#{thing.id}".to_sym,
      message: "Syncing for #{account.name}#{thing.is_a?(SharedBrandConfig) ? ": #{thing.name}" : ''}"
    )
    progress.user = @current_user
    progress.reset!
    progress.process_job(new_config, job_type, { priority: Delayed::HIGH_PRIORITY, strand: "brand_config_regenerate_#{thing.global_asset_string}" }, thing.id)

    @new_configs[config.md5] = new_config
    @progresses << progress
  end

  def process
    things_left_to_process = things_that_need_to_be_regenerated.dup
    while thing = things_left_to_process.sample
      next unless ready_to_process?(thing)
      regenerate(thing)
      things_left_to_process.delete(thing)
    end
  end

end
{'fraction_non_alphanumeric': 0.08161234137845236, 'fraction_numerical': 0.007713361532719582, 'mean_word_length': 4.062342569269521, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2112550', 'n_tokens_mistral': 2567, 'n_tokens_neox': 2501, 'n_words': 655}