dalli 0.9.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

@@ -0,0 +1,70 @@
1
+ require 'thread'
2
+
3
+ module Dalli
4
+
5
+ # Auto-marshal all values in/out of memcached.
6
+ # Otherwise, Dalli will just use to_s on all values.
7
+ #
8
+ # Dalli::Client.extend(Dalli::Marshal)
9
+ #
10
+ module Marshal
11
+ def serialize(value)
12
+ ::Marshal.dump(value)
13
+ end
14
+
15
+ def deserialize(value)
16
+ begin
17
+ ::Marshal.load(value)
18
+ rescue TypeError
19
+ raise Dalli::DalliError, "Invalid marshalled data in memcached, this happens if you switch the :marshal option and still have old data in memcached: #{value}"
20
+ end
21
+ end
22
+
23
+ def append(key, value)
24
+ raise Dalli::DalliError, "Marshalling and append do not work together"
25
+ end
26
+
27
+ def prepend(key, value)
28
+ raise Dalli::DalliError, "Marshalling and prepend do not work together"
29
+ end
30
+ end
31
+
32
+ # Make Dalli threadsafe by using a lock around all
33
+ # public server methods.
34
+ #
35
+ # Dalli::Server.extend(Dalli::Threadsafe)
36
+ #
37
+ module Threadsafe
38
+ def request(op, *args)
39
+ lock.synchronize do
40
+ super
41
+ end
42
+ end
43
+
44
+ def alive?
45
+ lock.synchronize do
46
+ super
47
+ end
48
+ end
49
+
50
+ def close
51
+ lock.synchronize do
52
+ super
53
+ end
54
+ end
55
+
56
+ def lock!
57
+ lock.mon_enter
58
+ end
59
+
60
+ def unlock!
61
+ lock.mon_exit
62
+ end
63
+
64
+ private
65
+ def lock
66
+ @lock ||= Monitor.new
67
+ end
68
+
69
+ end
70
+ end
data/lib/dalli/ring.rb ADDED
@@ -0,0 +1,99 @@
1
+ require 'digest/sha1'
2
+ require 'zlib'
3
+
4
+ module Dalli
5
+ class Ring
6
+ POINTS_PER_SERVER = 160 # this is the default in libmemcached
7
+
8
+ attr_accessor :servers, :continuum
9
+
10
+ def initialize(servers)
11
+ @servers = servers
12
+ if servers.size > 1
13
+ total_weight = servers.inject(0) { |memo, srv| memo + srv.weight }
14
+ continuum = []
15
+ servers.each do |server|
16
+ entry_count_for(server, servers.size, total_weight).times do |idx|
17
+ hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
18
+ value = Integer("0x#{hash[0..7]}")
19
+ continuum << Dalli::Ring::Entry.new(value, server)
20
+ end
21
+ end
22
+ continuum.sort { |a, b| a.value <=> b.value }
23
+ @continuum = continuum
24
+ end
25
+ end
26
+
27
+ def server_for_key(key)
28
+ return @servers.first unless @continuum
29
+
30
+ hkey = hash_for(key)
31
+
32
+ 20.times do |try|
33
+ entryidx = self.class.binary_search(@continuum, hkey)
34
+ server = @continuum[entryidx].server
35
+ return server if server.alive?
36
+ hkey = hash_for("#{try}#{key}")
37
+ end
38
+
39
+ raise Dalli::NetworkError, "No servers available"
40
+ end
41
+
42
+ def threadsafe!
43
+ @servers.each do |s|
44
+ s.extend(Dalli::Threadsafe)
45
+ end
46
+ end
47
+
48
+ def lock
49
+ @servers.each { |s| s.lock! }
50
+ begin
51
+ return yield
52
+ ensure
53
+ @servers.each { |s| s.unlock! }
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def hash_for(key)
60
+ Zlib.crc32(key)
61
+ end
62
+
63
+ def entry_count_for(server, total_servers, total_weight)
64
+ ((total_servers * POINTS_PER_SERVER * server.weight) / Float(total_weight)).floor
65
+ end
66
+
67
+ # Find the closest index in the Ring with value <= the given value
68
+ def self.binary_search(ary, value)
69
+ upper = ary.size - 1
70
+ lower = 0
71
+ idx = 0
72
+
73
+ while (lower <= upper) do
74
+ idx = (lower + upper) / 2
75
+ comp = ary[idx].value <=> value
76
+
77
+ if comp == 0
78
+ return idx
79
+ elsif comp > 0
80
+ upper = idx - 1
81
+ else
82
+ lower = idx + 1
83
+ end
84
+ end
85
+ return upper
86
+ end
87
+
88
+ class Entry
89
+ attr_reader :value
90
+ attr_reader :server
91
+
92
+ def initialize(val, srv)
93
+ @value = val
94
+ @server = srv
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,303 @@
1
+ require 'socket'
2
+
3
+ module Dalli
4
+ class Server
5
+ attr_accessor :hostname
6
+ attr_accessor :port
7
+ attr_accessor :weight
8
+
9
+ def initialize(attribs)
10
+ (@hostname, @port, @weight) = attribs.split(':')
11
+ @port ||= 11211
12
+ @port = Integer(@port)
13
+ @weight ||= 1
14
+ @weight = Integer(@weight)
15
+ connection
16
+ Dalli.logger.debug { "#{@hostname}:#{@port} running memcached v#{request(:version)}" }
17
+ end
18
+
19
+ def request(op, *args)
20
+ begin
21
+ send(op, *args)
22
+ rescue Dalli::NetworkError
23
+ raise
24
+ rescue Dalli::DalliError
25
+ raise
26
+ rescue Exception => ex
27
+ puts "Unexpected exception: #{ex.class.name}: #{ex.message}"
28
+ puts ex.backtrace.join("\n")
29
+ down!
30
+ end
31
+ end
32
+
33
+ def alive?
34
+ @sock && !@sock.closed?
35
+ end
36
+
37
+ def close
38
+ (@sock.close rescue nil; @sock = nil) if @sock
39
+ end
40
+
41
+ def lock!
42
+ end
43
+
44
+ def unlock!
45
+ end
46
+
47
+ private
48
+
49
+ def down!
50
+ close
51
+ @down_at = Time.now.to_i
52
+ @msg = $!.message
53
+ nil
54
+ end
55
+
56
+ ONE_MB = 1024 * 1024
57
+
58
+ def get(key)
59
+ req = [REQUEST, OPCODES[:get], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:get])
60
+ write(req)
61
+ generic_response
62
+ end
63
+
64
+ def getkq(key)
65
+ req = [REQUEST, OPCODES[:getkq], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:getkq])
66
+ write(req)
67
+ end
68
+
69
+ def set(key, value, ttl=0)
70
+ raise Dalli::DalliError, "Value too large, memcached can only store 1MB of data per key" if value.size > ONE_MB
71
+
72
+ req = [REQUEST, OPCODES[:set], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:set])
73
+ write(req)
74
+ generic_response
75
+ end
76
+
77
+ def flush(ttl)
78
+ req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
79
+ write(req)
80
+ generic_response
81
+ end
82
+
83
+ def add(key, value, ttl)
84
+ raise Dalli::DalliError, "Value too large, memcached can only store 1MB of data per key" if value.size > ONE_MB
85
+
86
+ req = [REQUEST, OPCODES[:add], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:add])
87
+ write(req)
88
+ generic_response
89
+ end
90
+
91
+ def append(key, value)
92
+ req = [REQUEST, OPCODES[:append], key.size, 0, 0, 0, value.size + key.size, 0, 0, key, value].pack(FORMAT[:append])
93
+ write(req)
94
+ generic_response
95
+ end
96
+
97
+ def delete(key)
98
+ req = [REQUEST, OPCODES[:delete], key.size, 0, 0, 0, key.size, 0, 0, key].pack(FORMAT[:delete])
99
+ write(req)
100
+ generic_response
101
+ end
102
+
103
+ def decr(key, count)
104
+ raise NotImplementedError
105
+ end
106
+
107
+ def incr(key, count)
108
+ raise NotImplementedError
109
+ end
110
+
111
+ # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
112
+ # We need to read all the responses at once.
113
+ def noop
114
+ req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
115
+ write(req)
116
+ multi_response
117
+ end
118
+
119
+ def prepend(key, value)
120
+ req = [REQUEST, OPCODES[:prepend], key.size, 0, 0, 0, value.size + key.size, 0, 0, key, value].pack(FORMAT[:prepend])
121
+ write(req)
122
+ generic_response
123
+ end
124
+
125
+ def replace(key, value, ttl)
126
+ req = [REQUEST, OPCODES[:replace], key.size, 8, 0, 0, value.size + key.size + 8, 0, 0, 0, ttl, key, value].pack(FORMAT[:replace])
127
+ write(req)
128
+ generic_response
129
+ end
130
+
131
+ def version
132
+ req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
133
+ write(req)
134
+ generic_response
135
+ end
136
+
137
+ def stats(info='')
138
+ req = [REQUEST, OPCODES[:stat], info.size, 0, 0, 0, info.size, 0, 0, info].pack(FORMAT[:stat])
139
+ write(req)
140
+ keyvalue_response
141
+ end
142
+
143
+ def generic_response
144
+ header = read(24)
145
+ raise Dalli::NetworkError, 'No response' if !header
146
+ (extras, status, count) = header.unpack(NORMAL_HEADER)
147
+ data = read(count) if count > 0
148
+ if status == 1
149
+ nil
150
+ elsif status != 0
151
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
152
+ elsif data
153
+ extras != 0 ? data[extras..-1] : data
154
+ else
155
+ true
156
+ end
157
+ end
158
+
159
+ def keyvalue_response
160
+ hash = {}
161
+ loop do
162
+ header = read(24)
163
+ raise Dalli::NetworkError, 'No response' if !header
164
+ (key_length, status, body_length) = header.unpack(KV_HEADER)
165
+ return hash if key_length == 0
166
+ key = read(key_length)
167
+ value = read(body_length - key_length) if body_length - key_length > 0
168
+ hash[key] = value
169
+ end
170
+ end
171
+
172
+ def multi_response
173
+ hash = {}
174
+ loop do
175
+ header = read(24)
176
+ raise Dalli::NetworkError, 'No response' if !header
177
+ (key_length, status, body_length) = header.unpack(KV_HEADER)
178
+ return hash if key_length == 0
179
+ read(4)
180
+ key = read(key_length)
181
+ value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
182
+ hash[key] = value
183
+ end
184
+ end
185
+
186
+ TIMEOUT = 0.5
187
+
188
+ def connection
189
+ @sock ||= begin
190
+ if @down_at && @down_at == Time.now.to_i
191
+ raise Dalli::NetworkError, "#{self.hostname}:#{self.port} is currently down: #{@msg}"
192
+ end
193
+
194
+ # All this ugly code to ensure proper Socket connect timeout
195
+ addr = Socket.getaddrinfo(self.hostname, nil)
196
+ sock = Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
197
+ begin
198
+ sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
199
+ rescue Errno::EINPROGRESS
200
+ resp = IO.select(nil, [sock], nil, TIMEOUT)
201
+ begin
202
+ sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
203
+ rescue Errno::EISCONN
204
+ ;
205
+ rescue
206
+ raise Dalli::NetworkError, "#{self.hostname}:#{self.port} is currently down: #{$!.message}"
207
+ end
208
+ end
209
+ # end ugly code
210
+
211
+ sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
212
+ sock
213
+ end
214
+ end
215
+
216
+ def write(bytes)
217
+ begin
218
+ connection.write(bytes)
219
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF
220
+ down!
221
+ raise Dalli::NetworkError, $!.class.name
222
+ end
223
+ end
224
+
225
+ def read(count)
226
+ begin
227
+ value = ''
228
+ begin
229
+ loop do
230
+ value << connection.sysread(count)
231
+ break if value.size == count
232
+ end
233
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
234
+ if IO.select([connection], nil, nil, TIMEOUT)
235
+ retry
236
+ else
237
+ raise Timeout::Error, "IO timeout"
238
+ end
239
+ end
240
+ raise Errno::EINVAL, "Not enough data to fulfill read request: #{value.inspect}" if value.size != count
241
+ value
242
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, Timeout::Error, EOFError
243
+ down!
244
+ raise Dalli::NetworkError, "#{$!.class.name}: #{$!.message}"
245
+ end
246
+ end
247
+
248
+ NORMAL_HEADER = '@4vnN'
249
+ KV_HEADER = '@2n@6nN'
250
+
251
+ REQUEST = 0x80
252
+ RESPONSE = 0x81
253
+
254
+ RESPONSE_CODES = {
255
+ 0 => 'No error',
256
+ 1 => 'Key not found',
257
+ 2 => 'Key exists',
258
+ 3 => 'Value too large',
259
+ 4 => 'Invalid arguments',
260
+ 5 => 'Item not stored',
261
+ 6 => 'Incr/decr on a non-numeric value',
262
+ 0x81 => 'Unknown command',
263
+ 0x82 => 'Out of memory',
264
+ }
265
+
266
+ OPCODES = {
267
+ :get => 0x00,
268
+ :set => 0x01,
269
+ :add => 0x02,
270
+ :replace => 0x03,
271
+ :delete => 0x04,
272
+ :incr => 0x05,
273
+ :decr => 0x06,
274
+ :flush => 0x08,
275
+ :noop => 0x0A,
276
+ :version => 0x0B,
277
+ :getkq => 0x0D,
278
+ :append => 0x0E,
279
+ :prepend => 0x0F,
280
+ :stat => 0x10,
281
+ }
282
+
283
+ HEADER = "CCnCCnNNQ"
284
+ OP_FORMAT = {
285
+ :get => 'a*',
286
+ :set => 'NNa*a*',
287
+ :add => 'NNa*a*',
288
+ :replace => 'NNa*a*',
289
+ :delete => 'a*',
290
+ :incr => 'NNNNNa*',
291
+ :decr => 'NNNNNa*',
292
+ :flush => 'N',
293
+ :noop => '',
294
+ :getkq => 'a*',
295
+ :version => '',
296
+ :stat => 'a*',
297
+ :append => 'a*a*',
298
+ :prepend => 'a*a*',
299
+ }
300
+ FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
301
+
302
+ end
303
+ end