aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/cache/mem_cache_store.rb
blob: eed9faac6d85c35370206bf54d4685e5b7295878 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
require 'memcache'

module ActiveSupport
  module Cache
    # A cache store implementation which stores data in Memcached:
    # http://www.danga.com/memcached/
    #
    # This is currently the most popular cache store for production websites.
    #
    # Special features:
    # - Clustering and load balancing. One can specify multiple memcached servers,
    #   and MemCacheStore will load balance between all available servers. If a
    #   server goes down, then MemCacheStore will ignore it until it goes back
    #   online.
    # - Time-based expiry support. See #write and the +:expires_in+ option.
    # - Per-request in memory cache for all communication with the MemCache server(s).
    class MemCacheStore < Store
      module Response # :nodoc:
        STORED      = "STORED\r\n"
        NOT_STORED  = "NOT_STORED\r\n"
        EXISTS      = "EXISTS\r\n"
        NOT_FOUND   = "NOT_FOUND\r\n"
        DELETED     = "DELETED\r\n"
      end

      # this allows caching of the fact that there is nothing in the remote cache
      NULL = 'mem_cache_store:null'

      THREAD_LOCAL_KEY = :mem_cache_store_cache

      class LocalCache
        def initialize(app)
          @app = app
        end

        def call(env)
          Thread.current[THREAD_LOCAL_KEY] = MemoryStore.new
          @app.call(env)
        ensure
          Thread.current[THREAD_LOCAL_KEY] = nil
        end
      end

      attr_reader :addresses

      # Creates a new MemCacheStore object, with the given memcached server
      # addresses. Each address is either a host name, or a host-with-port string
      # in the form of "host_name:port". For example:
      #
      #   ActiveSupport::Cache::MemCacheStore.new("localhost", "server-downstairs.localnetwork:8229")
      #
      # If no addresses are specified, then MemCacheStore will connect to
      # localhost port 11211 (the default memcached port).
      def initialize(*addresses)
        addresses = addresses.flatten
        options = addresses.extract_options!
        addresses = ["localhost"] if addresses.empty?
        @addresses = addresses
        @data = MemCache.new(addresses, options)
      end

      def read(key, options = nil) # :nodoc:
        super

        value = local_cache && local_cache.read(key)
        if value == NULL
          nil
        elsif value.nil?
          value = @data.get(key, raw?(options))
          local_cache.write(key, value || NULL) if local_cache
          value
        else
          # forcing the value to be immutable
          value.dup
        end
      rescue MemCache::MemCacheError => e
        logger.error("MemCacheError (#{e}): #{e.message}")
        nil
      end

      # Writes a value to the cache.
      #
      # Possible options:
      # - +:unless_exist+ - set to true if you don't want to update the cache
      #   if the key is already set.
      # - +:expires_in+ - the number of seconds that this value may stay in
      #   the cache. See ActiveSupport::Cache::Store#write for an example.
      def write(key, value, options = nil)
        super
        method = options && options[:unless_exist] ? :add : :set
        # memcache-client will break the connection if you send it an integer
        # in raw mode, so we convert it to a string to be sure it continues working.
        value = value.to_s if raw?(options)
        local_cache.write(key, value || NULL) if local_cache
        response = @data.send(method, key, value, expires_in(options), raw?(options))
        response == Response::STORED
      rescue MemCache::MemCacheError => e
        logger.error("MemCacheError (#{e}): #{e.message}")
        false
      end

      def delete(key, options = nil) # :nodoc:
        super
        local_cache.write(key, NULL) if local_cache
        response = @data.delete(key, expires_in(options))
        response == Response::DELETED
      rescue MemCache::MemCacheError => e
        logger.error("MemCacheError (#{e}): #{e.message}")
        false
      end

      def exist?(key, options = nil) # :nodoc:
        # Doesn't call super, cause exist? in memcache is in fact a read
        # But who cares? Reading is very fast anyway
        # Local cache is checked first, if it doesn't know then memcache itself is read from
        value = local_cache.read(key) if local_cache
        if value == NULL
          false
        elsif value
          true
        else
          !read(key, options).nil?
        end
      end

      def increment(key, amount = 1) # :nodoc:
        log("incrementing", key, amount)

        response = @data.incr(key, amount)
        unless response == Response::NOT_FOUND
          local_cache.write(key, response.to_s) if local_cache
          response
        else
          nil
        end
      rescue MemCache::MemCacheError
        nil
      end

      def decrement(key, amount = 1) # :nodoc:
        log("decrement", key, amount)

        response = @data.decr(key, amount)
        unless response == Response::NOT_FOUND
          local_cache.write(key, response.to_s) if local_cache
          response
        else
          nil
        end
      rescue MemCache::MemCacheError
        nil
      end

      def delete_matched(matcher, options = nil) # :nodoc:
        # don't do any local caching at present, just pass
        # through and let the error happen
        super
        raise "Not supported by Memcache"
      end

      def clear
        local_cache.clear if local_cache
        @data.flush_all
      end

      def stats
        @data.stats
      end

      private
        def local_cache
          Thread.current[THREAD_LOCAL_KEY]
        end

        def expires_in(options)
          (options && options[:expires_in]) || 0
        end

        def raw?(options)
          options && options[:raw]
        end
    end
  end
end