jashmenn-dalli 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,516 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+ require 'zlib'
4
+
5
+ module Dalli
6
+ class Server
7
+ attr_accessor :hostname
8
+ attr_accessor :port
9
+ attr_accessor :weight
10
+ attr_accessor :options
11
+
12
+ DEFAULTS = {
13
+ # seconds between trying to contact a remote server
14
+ :down_retry_delay => 1,
15
+ # connect/read/write timeout for socket operations
16
+ :socket_timeout => 0.5,
17
+ # times a socket operation may fail before considering the server dead
18
+ :socket_max_failures => 2,
19
+ # amount of time to sleep between retries when a failure occurs
20
+ :socket_failure_delay => 0.01,
21
+ # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
22
+ :value_max_bytes => 1024 * 1024,
23
+ :username => nil,
24
+ :password => nil,
25
+ }
26
+
27
+ def initialize(attribs, options = {})
28
+ (@hostname, @port, @weight) = attribs.split(':')
29
+ @port ||= 11211
30
+ @port = Integer(@port)
31
+ @weight ||= 1
32
+ @weight = Integer(@weight)
33
+ @fail_count = 0
34
+ @down_at = nil
35
+ @last_down_at = nil
36
+ @options = DEFAULTS.merge(options)
37
+ @sock = nil
38
+ @msg = nil
39
+ end
40
+
41
+ # Chokepoint method for instrumentation
42
+ def request(op, *args)
43
+ raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" unless alive?
44
+ begin
45
+ send(op, *args)
46
+ rescue Dalli::NetworkError
47
+ raise
48
+ rescue Dalli::MarshalError => ex
49
+ Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
50
+ Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
51
+ Dalli.logger.error ex.backtrace.join("\n\t")
52
+ false
53
+ rescue Dalli::DalliError
54
+ raise
55
+ rescue Exception => ex
56
+ Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
57
+ Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
58
+ Dalli.logger.error ex.backtrace.join("\n\t")
59
+ down!
60
+ end
61
+ end
62
+
63
+ def alive?
64
+ return true if @sock
65
+
66
+ if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
67
+ time = @last_down_at + options[:down_retry_delay] - Time.now
68
+ Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time }
69
+ return false
70
+ end
71
+
72
+ connect
73
+ !!@sock
74
+ rescue Dalli::NetworkError
75
+ false
76
+ end
77
+
78
+ def close
79
+ return unless @sock
80
+ @sock.close rescue nil
81
+ @sock = nil
82
+ end
83
+
84
+ def lock!
85
+ end
86
+
87
+ def unlock!
88
+ end
89
+
90
+ # NOTE: Additional public methods should be overridden in Dalli::Threadsafe
91
+
92
+ private
93
+
94
+ def failure!
95
+ Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
96
+
97
+ @fail_count += 1
98
+ if @fail_count >= options[:socket_max_failures]
99
+ down!
100
+ else
101
+ sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
102
+ end
103
+ end
104
+
105
+ def down!
106
+ close
107
+
108
+ @last_down_at = Time.now
109
+
110
+ if @down_at
111
+ time = Time.now - @down_at
112
+ Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time }
113
+ else
114
+ @down_at = @last_down_at
115
+ Dalli.logger.warn { "#{hostname}:#{port} is down" }
116
+ end
117
+
118
+ @error = $! && $!.class.name
119
+ @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
120
+ raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}"
121
+ end
122
+
123
+ def up!
124
+ if @down_at
125
+ time = Time.now - @down_at
126
+ Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time }
127
+ end
128
+
129
+ @fail_count = 0
130
+ @down_at = nil
131
+ @last_down_at = nil
132
+ @msg = nil
133
+ @error = nil
134
+ end
135
+
136
+ def multi?
137
+ Thread.current[:dalli_multi]
138
+ end
139
+
140
+ def get(key)
141
+ req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
142
+ write(req)
143
+ generic_response(true)
144
+ end
145
+
146
+ def getkq(key)
147
+ req = [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
148
+ write(req)
149
+ end
150
+
151
+ def set(key, value, ttl, options)
152
+ (value, flags) = serialize(key, value, options)
153
+
154
+ req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:set])
155
+ write(req)
156
+ generic_response unless multi?
157
+ end
158
+
159
+ def add(key, value, ttl, cas, options)
160
+ (value, flags) = serialize(key, value, options)
161
+
162
+ req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:add])
163
+ write(req)
164
+ generic_response unless multi?
165
+ end
166
+
167
+ def replace(key, value, ttl, options)
168
+ (value, flags) = serialize(key, value, options)
169
+ req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:replace])
170
+ write(req)
171
+ generic_response unless multi?
172
+ end
173
+
174
+ def delete(key)
175
+ req = [REQUEST, OPCODES[multi? ? :deleteq : :delete], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:delete])
176
+ write(req)
177
+ generic_response unless multi?
178
+ end
179
+
180
+ def flush(ttl)
181
+ req = [REQUEST, OPCODES[:flush], 0, 4, 0, 0, 4, 0, 0, 0].pack(FORMAT[:flush])
182
+ write(req)
183
+ generic_response
184
+ end
185
+
186
+ def decr(key, count, ttl, default)
187
+ expiry = default ? ttl : 0xFFFFFFFF
188
+ default ||= 0
189
+ (h, l) = split(count)
190
+ (dh, dl) = split(default)
191
+ req = [REQUEST, OPCODES[:decr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:decr])
192
+ write(req)
193
+ body = generic_response
194
+ body ? longlong(*body.unpack('NN')) : body
195
+ end
196
+
197
+ def incr(key, count, ttl, default)
198
+ expiry = default ? ttl : 0xFFFFFFFF
199
+ default ||= 0
200
+ (h, l) = split(count)
201
+ (dh, dl) = split(default)
202
+ req = [REQUEST, OPCODES[:incr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:incr])
203
+ write(req)
204
+ body = generic_response
205
+ body ? longlong(*body.unpack('NN')) : body
206
+ end
207
+
208
+ # Noop is a keepalive operation but also used to demarcate the end of a set of pipelined commands.
209
+ # We need to read all the responses at once.
210
+ def noop
211
+ req = [REQUEST, OPCODES[:noop], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
212
+ write(req)
213
+ multi_response
214
+ end
215
+
216
+ def append(key, value)
217
+ req = [REQUEST, OPCODES[:append], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:append])
218
+ write(req)
219
+ generic_response
220
+ end
221
+
222
+ def prepend(key, value)
223
+ req = [REQUEST, OPCODES[:prepend], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:prepend])
224
+ write(req)
225
+ generic_response
226
+ end
227
+
228
+ def stats(info='')
229
+ req = [REQUEST, OPCODES[:stat], info.bytesize, 0, 0, 0, info.bytesize, 0, 0, info].pack(FORMAT[:stat])
230
+ write(req)
231
+ keyvalue_response
232
+ end
233
+
234
+ def cas(key)
235
+ req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
236
+ write(req)
237
+ cas_response
238
+ end
239
+
240
+ def version
241
+ req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
242
+ write(req)
243
+ generic_response
244
+ end
245
+
246
+ COMPRESSION_MIN_SIZE = 1024
247
+
248
+ # http://www.hjp.at/zettel/m/memcached_flags.rxml
249
+ # Looks like most clients use bit 0 to indicate native language serialization
250
+ # and bit 1 to indicate gzip compression.
251
+ FLAG_MARSHALLED = 0x1
252
+ FLAG_COMPRESSED = 0x2
253
+
254
+ def serialize(key, value, options=nil)
255
+ marshalled = false
256
+ value = unless options && options[:raw]
257
+ marshalled = true
258
+ begin
259
+ Marshal.dump(value)
260
+ rescue => ex
261
+ # Marshalling can throw several different types of generic Ruby exceptions.
262
+ # Convert to a specific exception so we can special case it higher up the stack.
263
+ exc = Dalli::MarshalError.new(ex.message)
264
+ exc.set_backtrace ex.backtrace
265
+ raise exc
266
+ end
267
+ else
268
+ value.to_s
269
+ end
270
+ compressed = false
271
+ if @options[:compression] && value.bytesize >= COMPRESSION_MIN_SIZE
272
+ value = Zlib::Deflate.deflate(value)
273
+ compressed = true
274
+ end
275
+ raise Dalli::DalliError, "Value too large, memcached can only store #{@options[:value_max_bytes]} bytes per key [key: #{key}, size: #{value.bytesize}]" if value.bytesize > @options[:value_max_bytes]
276
+ flags = 0
277
+ flags |= FLAG_COMPRESSED if compressed
278
+ flags |= FLAG_MARSHALLED if marshalled
279
+ [value, flags]
280
+ end
281
+
282
+ def deserialize(value, flags)
283
+ value = Zlib::Inflate.inflate(value) if (flags & FLAG_COMPRESSED) != 0
284
+ value = Marshal.load(value) if (flags & FLAG_MARSHALLED) != 0
285
+ value
286
+ rescue TypeError, ArgumentError
287
+ raise DalliError, "Unable to unmarshal value: #{$!.message}"
288
+ rescue Zlib::Error
289
+ raise DalliError, "Unable to uncompress value: #{$!.message}"
290
+ end
291
+
292
+ def cas_response
293
+ header = read(24)
294
+ raise Dalli::NetworkError, 'No response' if !header
295
+ (extras, type, status, count, _, cas) = header.unpack(CAS_HEADER)
296
+ data = read(count) if count > 0
297
+ if status == 1
298
+ nil
299
+ elsif status != 0
300
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
301
+ elsif data
302
+ flags = data[0...extras].unpack('N')[0]
303
+ value = data[extras..-1]
304
+ data = deserialize(value, flags)
305
+ end
306
+ [data, cas]
307
+ end
308
+
309
+ CAS_HEADER = '@4CCnNNQ'
310
+ NORMAL_HEADER = '@4CCnN'
311
+ KV_HEADER = '@2n@6nN'
312
+
313
+ def generic_response(unpack=false)
314
+ header = read(24)
315
+ raise Dalli::NetworkError, 'No response' if !header
316
+ (extras, type, status, count) = header.unpack(NORMAL_HEADER)
317
+ data = read(count) if count > 0
318
+ if status == 1
319
+ nil
320
+ elsif status == 2 || status == 5
321
+ false # Not stored, normal status for add operation
322
+ elsif status != 0
323
+ raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
324
+ elsif data
325
+ flags = data[0...extras].unpack('N')[0]
326
+ value = data[extras..-1]
327
+ unpack ? deserialize(value, flags) : value
328
+ else
329
+ true
330
+ end
331
+ end
332
+
333
+ def keyvalue_response
334
+ hash = {}
335
+ loop do
336
+ header = read(24)
337
+ raise Dalli::NetworkError, 'No response' if !header
338
+ (key_length, status, body_length) = header.unpack(KV_HEADER)
339
+ return hash if key_length == 0
340
+ key = read(key_length)
341
+ value = read(body_length - key_length) if body_length - key_length > 0
342
+ hash[key] = value
343
+ end
344
+ end
345
+
346
+ def multi_response
347
+ hash = {}
348
+ loop do
349
+ header = read(24)
350
+ raise Dalli::NetworkError, 'No response' if !header
351
+ (key_length, status, body_length) = header.unpack(KV_HEADER)
352
+ return hash if key_length == 0
353
+ flags = read(4).unpack('N')[0]
354
+ key = read(key_length)
355
+ value = read(body_length - key_length - 4) if body_length - key_length - 4 > 0
356
+ hash[key] = deserialize(value, flags)
357
+ end
358
+ end
359
+
360
+ def write(bytes)
361
+ begin
362
+ @sock.write(bytes)
363
+ rescue SystemCallError
364
+ failure!
365
+ retry
366
+ end
367
+ end
368
+
369
+ def read(count)
370
+ begin
371
+ @sock.readfull(count)
372
+ rescue SystemCallError, Timeout::Error, EOFError
373
+ failure!
374
+ retry
375
+ end
376
+ end
377
+
378
+ def connect
379
+ Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" }
380
+
381
+ begin
382
+ @sock = KSocket.open(hostname, port, :timeout => options[:socket_timeout])
383
+ @version = version # trigger actual connect
384
+ sasl_authentication if need_auth?
385
+ up!
386
+ rescue Dalli::DalliError # SASL auth failure
387
+ raise
388
+ rescue SystemCallError, Timeout::Error, EOFError
389
+ failure!
390
+ retry
391
+ end
392
+ end
393
+
394
+ def split(n)
395
+ [n >> 32, 0xFFFFFFFF & n]
396
+ end
397
+
398
+ def longlong(a, b)
399
+ (a << 32) | b
400
+ end
401
+
402
+ REQUEST = 0x80
403
+ RESPONSE = 0x81
404
+
405
+ RESPONSE_CODES = {
406
+ 0 => 'No error',
407
+ 1 => 'Key not found',
408
+ 2 => 'Key exists',
409
+ 3 => 'Value too large',
410
+ 4 => 'Invalid arguments',
411
+ 5 => 'Item not stored',
412
+ 6 => 'Incr/decr on a non-numeric value',
413
+ 0x20 => 'Authentication required',
414
+ 0x81 => 'Unknown command',
415
+ 0x82 => 'Out of memory',
416
+ }
417
+
418
+ OPCODES = {
419
+ :get => 0x00,
420
+ :set => 0x01,
421
+ :add => 0x02,
422
+ :replace => 0x03,
423
+ :delete => 0x04,
424
+ :incr => 0x05,
425
+ :decr => 0x06,
426
+ :flush => 0x08,
427
+ :noop => 0x0A,
428
+ :version => 0x0B,
429
+ :getkq => 0x0D,
430
+ :append => 0x0E,
431
+ :prepend => 0x0F,
432
+ :stat => 0x10,
433
+ :setq => 0x11,
434
+ :addq => 0x12,
435
+ :replaceq => 0x13,
436
+ :deleteq => 0x14,
437
+ :incrq => 0x15,
438
+ :decrq => 0x16,
439
+ :auth_negotiation => 0x20,
440
+ :auth_request => 0x21,
441
+ :auth_continue => 0x22,
442
+ }
443
+
444
+ HEADER = "CCnCCnNNQ"
445
+ OP_FORMAT = {
446
+ :get => 'a*',
447
+ :set => 'NNa*a*',
448
+ :add => 'NNa*a*',
449
+ :replace => 'NNa*a*',
450
+ :delete => 'a*',
451
+ :incr => 'NNNNNa*',
452
+ :decr => 'NNNNNa*',
453
+ :flush => 'N',
454
+ :noop => '',
455
+ :getkq => 'a*',
456
+ :version => '',
457
+ :stat => 'a*',
458
+ :append => 'a*a*',
459
+ :prepend => 'a*a*',
460
+ :auth_request => 'a*a*',
461
+ :auth_continue => 'a*a*',
462
+ }
463
+ FORMAT = OP_FORMAT.inject({}) { |memo, (k, v)| memo[k] = HEADER + v; memo }
464
+
465
+
466
+ #######
467
+ # SASL authentication support for NorthScale
468
+ #######
469
+
470
+ def need_auth?
471
+ @options[:username] || ENV['MEMCACHE_USERNAME']
472
+ end
473
+
474
+ def username
475
+ @options[:username] || ENV['MEMCACHE_USERNAME']
476
+ end
477
+
478
+ def password
479
+ @options[:password] || ENV['MEMCACHE_PASSWORD']
480
+ end
481
+
482
+ def sasl_authentication
483
+ Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
484
+
485
+ # negotiate
486
+ req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
487
+ write(req)
488
+ header = read(24)
489
+ raise Dalli::NetworkError, 'No response' if !header
490
+ (extras, type, status, count) = header.unpack(NORMAL_HEADER)
491
+ raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
492
+ content = read(count)
493
+ return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
494
+ mechanisms = content.split(' ')
495
+ raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN')
496
+
497
+ # request
498
+ mechanism = 'PLAIN'
499
+ msg = "\x0#{username}\x0#{password}"
500
+ req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
501
+ write(req)
502
+
503
+ header = read(24)
504
+ raise Dalli::NetworkError, 'No response' if !header
505
+ (extras, type, status, count) = header.unpack(NORMAL_HEADER)
506
+ raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
507
+ content = read(count)
508
+ return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
509
+
510
+ raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
511
+ raise NotImplementedError, "No two-step authentication mechanisms supported"
512
+ # (step, msg) = sasl.receive('challenge', content)
513
+ # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
514
+ end
515
+ end
516
+ end