af83/chouette-core

View on GitHub
app/lib/chouette/checksum_manager.rb

Summary

Maintainability
B
6 hrs
Test Coverage
module Chouette::ChecksumManager
  THREAD_VARIABLE_NAME = "current_checksum_manager".freeze

  class NotInTransactionError < StandardError; end
  class AlreadyInTransactionError < StandardError; end
  class MultipleReferentialsError < StandardError; end

  def self.current
    current_manager = Thread.current.thread_variable_get THREAD_VARIABLE_NAME
    current_manager || self.current = Chouette::ChecksumManager::NoUpdates.new
  end

  def self.current= manager
    Thread.current.thread_variable_set THREAD_VARIABLE_NAME, manager
    manager
  end

  def self.cleanup
    Thread.current.thread_variable_set THREAD_VARIABLE_NAME, nil
  end

  def self.logger
    @@logger ||= Rails.logger
  end

  def self.logger= logger
    @@logger = logger
  end

  def self.log_level
    @@log_level ||= :debug
  end

  def self.log_level= log_level
    @@log_level = log_level if logger.respond_to?(log_level)
  end

  def self.log msg
    prefix = "[ChecksumManager::#{current.class.name.split('::').last} #{current.object_id.to_s(16)}]"
    logger.send log_level, "#{prefix} #{msg}"
  end

  def self.start_transaction
    return if in_no_updates?
    return unless transaction_enabled?

    raise AlreadyInTransactionError if in_transaction?
    self.current = Chouette::ChecksumManager::Transactional.new
    log "=== NEW TRANSACTION ==="
  end

  def self.in_transaction?
    current.is_a?(Chouette::ChecksumManager::Transactional)
  end

  def self.in_no_updates?
    current.is_a?(Chouette::ChecksumManager::NoUpdates)
  end

  def self.commit
    return if in_no_updates?
    return unless transaction_enabled?

    current.log "=== COMMITTING TRANSACTION ==="
    raise NotInTransactionError unless in_transaction?
    current.commit
    log "=== DONE COMMITTING TRANSACTION ==="
    self.current = nil
  end

  def self.after_create object
    current.after_create object
  end

  def self.after_destroy object
    current.after_destroy object
  end

  def self.transaction_enabled?
    Rails.application.config.enable_transactional_checksums
  end

  def self.start_no_updates
    self.current = Chouette::ChecksumManager::NoUpdates.new
    log "=== NO CHECKSUM UPDATES ==="
    log "=== BE CAREFUL, YOU WILL NEED TO UPDATE CHECKSUMS MANUALLY ==="
  end

  def self.commit_no_updates
    log "=== ENDING NO CHECKSUM UPDATES ==="
    self.current = nil
  end

  def self.no_updates
    begin
      start_no_updates
      out = yield
      commit_no_updates
      out
    rescue
      commit_no_updates
      raise
    end
  end

  def self.transaction
    begin
      start_transaction
      out = yield
      commit
      out
    rescue
      commit
      raise
    end
  end

  def self.inline
    begin
      self.current = Chouette::ChecksumManager::Inline.new
      yield
    ensure
      self.current = nil
    end
  end

  def self.update_checkum_in_batches(collection, referential)
    collection.find_in_batches do |group|
      ids = []
      checksums = []
      checksum_sources = []
      group.each do |r|
        ids << r.id
        source = r.current_checksum_source(db_lookup: false)
        checksum_sources << ActiveRecord::Base.sanitize_sql(source).gsub(/'/, "''")
        checksums << Digest::SHA256.new.hexdigest(source)
      end
      sql = <<SQL
        UPDATE \"#{referential.slug}\".#{collection.klass.table_name} tmp SET checksum_source = data_table.checksum_source, checksum = data_table.checksum
        FROM
        (select unnest(array[#{ids.join(",")}]) as id,
        unnest(array['#{checksums.join("','")}']) as checksum,
        unnest(array['#{checksum_sources.join("','")}']) as checksum_source) as data_table
        where tmp.id = data_table.id;
SQL
      ActiveRecord::Base.connection.execute sql
    end
  end

  def self.watch object, from: nil
    current.watch object, from: from
  end

  def self.object_signature object
    SerializedObject.new(object).signature
  end

  def self.checksum_parents object
    klass = object.class
    return [] unless klass.respond_to? :checksum_parent_relations
    return [] unless klass.checksum_parent_relations

    parents = []
    klass.checksum_parent_relations.each do |parent_model, opts|
      belongs_to = opts[:relation] || parent_model.model_name.singular
      has_many = opts[:relation] || parent_model.model_name.plural

      if object.respond_to? belongs_to
        reflection = klass.reflections[belongs_to.to_s]
        if reflection
          if object.association(belongs_to.intern).loaded?
            log "parent is already loaded"
            parent = object.send(belongs_to)
            parents << SerializedObject.new(parent, need_save: true, load_object: true) if parent
          else
            log "parent is not loaded but can be inferred from reflection"
            parent_id = object.send(reflection.foreign_key)
            parent_class = reflection.klass.name
          end
        else
          # the relation is not a true ActiveRecord Relation
          log "parent has to be loaded"
          parent = object.send(belongs_to)
          parents << [parent.class.name, parent.id]
        end
        parents << [parent_class, parent_id] if parent_id
      end

      if object.respond_to? has_many
        # XXX: SOME OPTIM POSSIBLE HERE

        if reflection && object.association(has_many.intern).loaded?
          log "#{has_many} parents are already loaded"
          parents += object.send(has_many).map{|p| SerializedObject.new(p, need_save: true)}
        else
          if reflection && !reflection.options[:through]
            log "#{has_many} parent are not loaded but can be inferred from reflection"
            parents += [reflection.klass.name].product(object.send(has_many).pluck(reflection.foreign_key).compact)
          else
            log "#{has_many} parents have to be loaded"
            # the relation is not a true ActiveRecord Relation
            parents += object.send(has_many).map { |p| SerializedObject.new(p, need_save: true, load_object: true)}
          end
        end
      end
    end

    parents.compact
  end

  def self.parents_to_sentence parents
    parents.map do |p|
      if p.is_a?(Array)
        p
      elsif p.respond_to?(:serialized_object)
        p.serialized_object
      else
       [p.class.name, p.id]
     end
    end.group_by(&:first).map{ |klass, v| "#{v.size} #{klass}" }.to_sentence
  end

  def self.child_after_save object
    current.child_after_save(object)
  end

  def self.child_before_destroy object
    parents = checksum_parents object

    log "Prepare request for #{object.class.name}##{object.id} deletion checksum updates for #{parents.count} parent(s): #{parents_to_sentence(parents)}"

    @_parents_for_checksum_update ||= {}
    @_parents_for_checksum_update[object_signature(object)] = parents
  end

  def self.child_after_destroy object
    if @_parents_for_checksum_update.present? && @_parents_for_checksum_update[object_signature(object)].present?
      parents = @_parents_for_checksum_update[object_signature(object)]
      log "Request from #{object.class.name}##{object.id} checksum updates for #{parents.count} parent(s): #{parents_to_sentence(parents)}"
      parents.each { |parent| Chouette::ChecksumManager.watch parent, from: object }
      @_parents_for_checksum_update.delete object
    end
  end
end