Module: ActsAsReplaceable::HelperMethods

Defined in:
lib/acts_as_replaceable/acts_as_replaceable.rb

Class Method Summary collapse

Class Method Details

.copy_attributes(attributes, source, target) ⇒ Object



87
88
89
90
91
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 87

def self.copy_attributes(attributes, source, target)
  attributes.each do |attribute|
    target[attribute] = source[attribute]
  end
end

.find_existing(record) ⇒ Object

Searches the database for an existing copies of record



94
95
96
97
98
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 94

def self.find_existing(record)
  existing = record.class.default_scoped
  existing = existing.where match_conditions(record)
  existing = existing.where insensitive_match_conditions(record)
end

.insensitive_match_conditions(record) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 60

def self.insensitive_match_conditions(record)
  sql = []
  binds = []
  record.acts_as_replaceable_options[:insensitive_match].each do |attribute_name|
    if value = record[attribute_name]
      sql << "LOWER(#{attribute_name}) = ?"
      binds << record[attribute_name].downcase
    else
      sql << "#{attribute_name} IS NULL"
    end
  end

  return nil if sql.empty?
  return [sql.join(' AND ')] + binds
end

.lock(record, timeout = 20) ⇒ Object

A lock is used to prevent multiple threads from executing the same query simultaneously eg. In a multi-threaded environment, ‘find_or_create’ is prone to failure due to the possibility that the process is preempted between the ‘find’ and ‘create’ logic



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 112

def self.lock(record, timeout = 20)
  lock_id  = "ActsAsReplaceable/#{OpenSSL::Digest::MD5.digest([match_conditions(record), insensitive_match_conditions(record)].inspect)}"
  acquired = false

  # Acquire the lock by atomically incrementing and returning the value to see if we're first
  while !acquired do
    unless acquired = Rails.cache.increment(lock_id) == 1
      puts "lock was in use #{lock_id}"
      sleep(0.250)
    end
  end

  # Reserve the lock for only 10 seconds more than the timeout to ensure a lock is always eventually released
  Rails.cache.write(lock_id, "1", :raw => true, :expires_in => timeout + 10)
  Timeout::timeout(timeout) do
    yield
  end

ensure # Give up the lock
  Rails.cache.write(lock_id, "0", :raw => true) if acquired
end

.lock_if(condition, *lock_args, &block) ⇒ Object

Conditionally lock (lets us enable or disable locking)



101
102
103
104
105
106
107
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 101

def self.lock_if(condition, *lock_args, &block)
  if condition
    lock(*lock_args, &block)
  else
    yield
  end
end

.mark_changes(record, existing) ⇒ Object

Copy attributes to existing and see how it would change if we updated it Mark all record’s attributes that have changed, so even if they are still default values, they will be saved to the database



79
80
81
82
83
84
85
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 79

def self.mark_changes(record, existing)
  copy_attributes(record.attribute_names, record, existing)

  existing.changed.each {|attribute| record.send("#{attribute}_will_change!") }

  return existing.changed?
end

.match_conditions(record) ⇒ Object

Search the incoming attributes for attributes that are in the replaceable conditions and use those to form an Find conditions



52
53
54
55
56
57
58
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 52

def self.match_conditions(record)
  output = {}
  record.acts_as_replaceable_options[:match].each do |attribute_name|
    output[attribute_name] = record[attribute_name]
  end
  return output
end

.sanitize_attribute_names(klass, *args) ⇒ Object



42
43
44
45
46
47
48
49
# File 'lib/acts_as_replaceable/acts_as_replaceable.rb', line 42

def self.sanitize_attribute_names(klass, *args)
  unless klass.table_exists?
    ActiveRecord::Base.logger.warn "(acts_as_replaceable) table `#{klass.table_name}` does not exist so excluding all attribute names"
    return []
  end
  # Intersect the proposed attributes with the column names so we don't start assigning attributes that don't exist. e.g. if the model doesn't have timestamps
  klass.column_names & args.flatten.compact.collect(&:to_s)
end