jashmenn-dalli 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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