Module: Interlock

Defined in:
lib/interlock/config.rb,
lib/interlock.rb,
lib/interlock/lock.rb,
lib/interlock/finders.rb,
lib/interlock/interlock.rb,
lib/interlock/pass_through_store.rb

Overview

require ‘ehcache’

Defined Under Namespace

Modules: Config, Finders, Lock Classes: ConfigurationError, DependencyError, FragmentConsistencyError, InterlockError, LockAcquisitionError, PassThroughStore, UsageError

Constant Summary collapse

DEFAULTS =
{
  :ttl => 1.day,
  :namespace => 'app',
  :servers => ['127.0.0.1:11211'],
  :client => 'memcache-client',
  :with_finders => false
}
CLIENT_KEYS =
[
  :prefix_key,
  :distribution,
  :verify_key,
  :tcp_nodelay,
  :hash,
  :hash_with_prefix_key,
  :show_backtraces,
  :default_ttl,
  :ketama_weighted,
  :retry_timeout,
  :default_weight,
  :buffer_requests,
  :timeout,
  :sort_hosts,
  :cache_lookups,
  :connect_timeout,
  :no_block,
  :failover,
  :support_cas,
  :namespace
]
SCOPE_KEYS =
[:controller, :action, :id]
KEY_LENGTH_LIMIT =

Buried value extracted from memcache.rb in the memcache-client gem. If one tries to request a key that is too long, the client throws an error. In practice, it seems better to simply avoid ever setting such long keys, so we use this value to achieve such for keys generated by Interlock.

250
ILLEGAL_KEY_CHARACTERS_PATTERN =

Similarly buried and useful: no whitespace allowed in keys.

/\s/
@@config =
DEFAULTS
@@local_cache =

Install the pass-through store. This is used in the console and test environment. In a server environment, the controller callbacks install the memory store before each request.

Interlock::PassThroughStore.new

Class Method Summary collapse

Class Method Details

.caching_key(controller, action, id, tag) ⇒ Object

Build a fragment key for an explicitly passed context. Shouldn’t be called unless you need to write your own fine-grained invalidation rules. Make sure the default ones are really unacceptable before you go to the trouble of rolling your own.

Raises:

  • (ArgumentError)


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/interlock/interlock.rb', line 128

def caching_key(controller, action, id, tag)
  raise ArgumentError, 'Both controller and action must be specified' unless controller and action
  
  id = (id or 'all').to_interlock_tag
  tag = tag.to_interlock_tag
  
  key = "interlock:#{ENV['RAILS_ASSET_ID']}:#{controller}:#{action}:#{id}:#{tag}"
  
  if key.length > KEY_LENGTH_LIMIT
    old_key = key
    key = key[0..KEY_LENGTH_LIMIT-1]
    say old_key, "truncated to #{key}"
  end
  
  key.gsub ILLEGAL_KEY_CHARACTERS_PATTERN, ''
end

.dependency_key(klass, scope, key) ⇒ Object

Get the Memcached key for a class’s dependency list. We store per-class to reduce lock contention.



117
118
119
120
# File 'lib/interlock/interlock.rb', line 117

def dependency_key(klass, scope, key) 
  id = (scope == :id ? ":#{key.field(4)}" : nil)
  "interlock:#{ENV['RAILS_ASSET_ID']}:dependency:#{klass.name}#{id}"
end

.extract_options_and_dependencies(dependencies, default = nil) ⇒ Object

Extract the dependencies from the rest of the arguments and registers them with the appropriate models.



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/interlock/interlock.rb', line 40

def extract_options_and_dependencies(dependencies, default = nil) 
  options = dependencies.extract_options!
  
  # Hook up the dependencies nested array.
  dependencies.map! { |klass| [klass, :all] }
  options.each do |klass, scope| 
    if klass.is_a? Class 
      #
      # Beware! Scoping to :id means that a request's params[:id] must equal 
      # klass#id or the rule will not trigger. This is because params[:id] is the
      # only record-specific scope include in the key. 
      #
      # If you want fancier invalidation, think hard about whether it really 
      # matters. Over-invalidation is rarely a problem, but under-invalidation
      # frequently is. 
      #
      # "But I need it!" you say. All right, then start using key tags.
      #
      raise Interlock::DependencyError, "#{scope.inspect} is not a valid scope" unless [:all, :id].include?(scope)
      dependencies << [klass, scope.to_sym]
    end
  end    

  unless dependencies.any?
    # Use the conventional controller/model association if none are provided
    # Can be skipped by calling caching(nil)
    dependencies = [[default, :all]]
  end
  
  # Remove nils
  dependencies.reject! {|klass, scope| klass.nil? }
  
  [options.indifferentiate, dependencies]
end

.invalidate(key) ⇒ Object

Invalidate a particular key.



148
149
150
151
152
# File 'lib/interlock/interlock.rb', line 148

def invalidate(key)
  # Console and tests do not install the local cache
  Interlock.local_cache.delete(key) if Interlock.local_cache
  ActionController::Base.cache_store.delete key
end

.log(msg) ⇒ Object



102
103
104
105
106
107
108
109
110
111
# File 'lib/interlock/interlock.rb', line 102

def log(msg)
  case Interlock.config[:log_level]
  when 'debug', 'info', 'warn', 'error'
    log_method = Interlock.config[:log_level]
  else
    log_method = :debug
  end
  
  RAILS_DEFAULT_LOGGER.send( log_method, msg )
end

.register_dependencies(dependencies, key) ⇒ Object

Add each key with scope to the appropriate dependencies array.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/interlock/interlock.rb', line 78

def register_dependencies(dependencies, key)
  return if Interlock.config[:disabled]
 
  Array(dependencies).each do |klass, scope|
    dep_key = dependency_key(klass, scope, key)
  
    # Get the value for this class/key out of the global store.
    this = (CACHE.get(dep_key) || {})[key]

    # Make sure to not overwrite broader scopes.
    unless this == :all or this == scope
      # We need to write, so acquire the lock.            
      CACHE.lock(dep_key) do |hash|
        Interlock.say key, "registered a dependency on #{klass} -> #{scope.inspect}."
        (hash || {}).merge({key => scope})
      end
    end        
  end
end

.say(key, msg, type = "fragment") ⇒ Object

:nodoc:



98
99
100
# File 'lib/interlock/interlock.rb', line 98

def say(key, msg, type = "fragment") #:nodoc:
  log "** #{type} #{key.inspect[1..-2]} #{msg}"
end