dalli 3.0.6 → 3.1.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.
- checksums.yaml +4 -4
- data/History.md +9 -0
- data/lib/dalli/client.rb +14 -147
- data/lib/dalli/options.rb +3 -3
- data/lib/dalli/pipelined_getter.rb +175 -0
- data/lib/dalli/protocol/binary/response_header.rb +36 -0
- data/lib/dalli/protocol/binary/response_processor.rb +72 -43
- data/lib/dalli/protocol/binary.rb +70 -55
- data/lib/dalli/protocol/response_buffer.rb +50 -0
- data/lib/dalli/ring.rb +5 -1
- data/lib/dalli/socket.rb +8 -6
- data/lib/dalli/version.rb +1 -1
- data/lib/dalli.rb +7 -0
- data/lib/rack/session/dalli.rb +83 -74
- metadata +13 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5450b3f6caa26344dbcf9718ee2e12c98d593df098d5aec6f41655783675e2b8
|
4
|
+
data.tar.gz: fdc8ff913351a2ed85fb34c0aa50506f5014e29c3671cbf312eaf7255b526742
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 226719cde318de90be8fb77160b926ddbc75002ab0632494c7de0d1917bc88a8fde01b0e827765db3baa5f439c42f840a298c546d56473109e65003509d452ff
|
7
|
+
data.tar.gz: 6b9ca93a048ae7a5074bd128432ceb1f19497e16c773a4a246ba1da13ecb1d7e9024c41f9e112ecea2244205afded043d893c8368fcf5c137b14f134b9f1548e
|
data/History.md
CHANGED
@@ -1,6 +1,15 @@
|
|
1
1
|
Dalli Changelog
|
2
2
|
=====================
|
3
3
|
|
4
|
+
3.1.0
|
5
|
+
==========
|
6
|
+
|
7
|
+
- BREAKING CHANGE: Update Rack::Session::Dalli to inherit from Abstract::PersistedSecure. This will invalidate existing sessions (petergoldstein)
|
8
|
+
- BREAKING CHANGE: Use of unsupported operations in a multi block now raise an error. (petergoldstein)
|
9
|
+
- Extract PipelinedGetter from Dalli::Client (petergoldstein)
|
10
|
+
- Fix SSL socket so that it works with pipelined gets (petergoldstein)
|
11
|
+
- Additional refactoring to split classes (petergoldstein)
|
12
|
+
|
4
13
|
3.0.6
|
5
14
|
==========
|
6
15
|
|
data/lib/dalli/client.rb
CHANGED
@@ -63,12 +63,12 @@ module Dalli
|
|
63
63
|
# pipelined as Dalli will use 'quiet' operations where possible.
|
64
64
|
# Currently supports the set, add, replace and delete operations.
|
65
65
|
def multi
|
66
|
-
old = Thread.current[
|
67
|
-
Thread.current[
|
66
|
+
old = Thread.current[::Dalli::MULTI_KEY]
|
67
|
+
Thread.current[::Dalli::MULTI_KEY] = true
|
68
68
|
yield
|
69
69
|
ensure
|
70
|
-
@ring&.
|
71
|
-
Thread.current[
|
70
|
+
@ring&.pipeline_consume_and_ignore_responses
|
71
|
+
Thread.current[::Dalli::MULTI_KEY] = old
|
72
72
|
end
|
73
73
|
|
74
74
|
##
|
@@ -89,10 +89,10 @@ module Dalli
|
|
89
89
|
return {} if keys.empty?
|
90
90
|
|
91
91
|
if block_given?
|
92
|
-
|
92
|
+
pipelined_getter.process(keys) { |k, data| yield k, data.first }
|
93
93
|
else
|
94
94
|
{}.tap do |hash|
|
95
|
-
|
95
|
+
pipelined_getter.process(keys) { |k, data| hash[k] = data.first }
|
96
96
|
end
|
97
97
|
end
|
98
98
|
end
|
@@ -306,10 +306,10 @@ module Dalli
|
|
306
306
|
# { 'key' => [value, cas_id] }
|
307
307
|
def get_multi_cas(*keys)
|
308
308
|
if block_given?
|
309
|
-
|
309
|
+
pipelined_getter.process(keys) { |*args| yield(*args) }
|
310
310
|
else
|
311
311
|
{}.tap do |hash|
|
312
|
-
|
312
|
+
pipelined_getter.process(keys) { |k, data| hash[k] = data }
|
313
313
|
end
|
314
314
|
end
|
315
315
|
end
|
@@ -341,9 +341,7 @@ module Dalli
|
|
341
341
|
# Close our connection to each server.
|
342
342
|
# If you perform another operation after this, the connections will be re-established.
|
343
343
|
def close
|
344
|
-
|
345
|
-
|
346
|
-
@ring.servers.each(&:close)
|
344
|
+
@ring&.close
|
347
345
|
@ring = nil
|
348
346
|
end
|
349
347
|
alias reset close
|
@@ -402,145 +400,14 @@ module Dalli
|
|
402
400
|
end
|
403
401
|
|
404
402
|
def normalize_options(opts)
|
405
|
-
|
406
|
-
opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
|
407
|
-
rescue NoMethodError
|
408
|
-
raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
|
409
|
-
end
|
403
|
+
opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
|
410
404
|
opts
|
405
|
+
rescue NoMethodError
|
406
|
+
raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
|
411
407
|
end
|
412
408
|
|
413
|
-
|
414
|
-
|
415
|
-
##
|
416
|
-
# Yields, one at a time, keys and their values+attributes.
|
417
|
-
#
|
418
|
-
def get_multi_yielder(keys, &block)
|
419
|
-
return {} if keys.empty?
|
420
|
-
|
421
|
-
ring.lock do
|
422
|
-
groups = groups_for_keys(keys)
|
423
|
-
if (unfound_keys = groups.delete(nil))
|
424
|
-
Dalli.logger.debug do
|
425
|
-
"unable to get keys for #{unfound_keys.length} keys "\
|
426
|
-
'because no matching server was found'
|
427
|
-
end
|
428
|
-
end
|
429
|
-
make_multi_get_requests(groups)
|
430
|
-
|
431
|
-
servers = groups.keys
|
432
|
-
return if servers.empty?
|
433
|
-
|
434
|
-
# TODO: How does this exit on a NetworkError
|
435
|
-
servers = perform_multi_response_start(servers)
|
436
|
-
|
437
|
-
timeout = servers.first.options[:socket_timeout]
|
438
|
-
start_time = Time.now
|
439
|
-
loop do
|
440
|
-
# remove any dead servers
|
441
|
-
# TODO: Is this well behaved in a multi-threaded environment?
|
442
|
-
# Accessing the server socket like this seems problematic
|
443
|
-
servers.delete_if { |s| s.sock.nil? }
|
444
|
-
break if servers.empty?
|
445
|
-
|
446
|
-
servers = multi_yielder_loop(servers, start_time, timeout, &block)
|
447
|
-
end
|
448
|
-
end
|
449
|
-
rescue NetworkError => e
|
450
|
-
Dalli.logger.debug { e.inspect }
|
451
|
-
Dalli.logger.debug { 'retrying multi yielder because of timeout' }
|
452
|
-
retry
|
453
|
-
end
|
454
|
-
|
455
|
-
def make_multi_get_requests(groups)
|
456
|
-
groups.each do |server, keys_for_server|
|
457
|
-
server.request(:send_multiget, keys_for_server)
|
458
|
-
rescue DalliError, NetworkError => e
|
459
|
-
Dalli.logger.debug { e.inspect }
|
460
|
-
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
461
|
-
end
|
462
|
-
end
|
463
|
-
|
464
|
-
# raises Dalli::NetworkError
|
465
|
-
def perform_multi_response_start(servers)
|
466
|
-
deleted = []
|
467
|
-
|
468
|
-
servers.each do |server|
|
469
|
-
next unless server.alive?
|
470
|
-
|
471
|
-
begin
|
472
|
-
server.multi_response_start
|
473
|
-
rescue Dalli::NetworkError
|
474
|
-
abort_multi_response(servers)
|
475
|
-
raise
|
476
|
-
rescue Dalli::DalliError => e
|
477
|
-
Dalli.logger.debug { e.inspect }
|
478
|
-
Dalli.logger.debug { 'results from this server will be missing' }
|
479
|
-
deleted.append(server)
|
480
|
-
end
|
481
|
-
end
|
482
|
-
|
483
|
-
servers.delete_if { |server| deleted.include?(server) }
|
484
|
-
end
|
485
|
-
|
486
|
-
# Swallows Dalli::NetworkError
|
487
|
-
def abort_multi_response(servers)
|
488
|
-
servers.each(&:multi_response_abort)
|
489
|
-
end
|
490
|
-
|
491
|
-
def multi_yielder_loop(servers, start_time, timeout, &block)
|
492
|
-
time_left = remaining_time(start_time, timeout)
|
493
|
-
readable_servers = servers_with_data(servers, time_left)
|
494
|
-
if readable_servers.empty?
|
495
|
-
abort_multi_connections_w_timeout(servers)
|
496
|
-
return readable_servers
|
497
|
-
end
|
498
|
-
|
499
|
-
readable_servers.each do |server|
|
500
|
-
servers.delete(server) if respond_to_readable_server(server, &block)
|
501
|
-
end
|
502
|
-
servers
|
503
|
-
rescue NetworkError
|
504
|
-
abort_multi_response(servers)
|
505
|
-
raise
|
506
|
-
end
|
507
|
-
|
508
|
-
def remaining_time(start, timeout)
|
509
|
-
elapsed = Time.now - start
|
510
|
-
return 0 if elapsed > timeout
|
511
|
-
|
512
|
-
timeout - elapsed
|
513
|
-
end
|
514
|
-
|
515
|
-
# Swallows Dalli::NetworkError
|
516
|
-
def abort_multi_connections_w_timeout(servers)
|
517
|
-
abort_multi_response(servers)
|
518
|
-
servers.each do |server|
|
519
|
-
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
520
|
-
end
|
521
|
-
|
522
|
-
true # Required to simplify caller
|
523
|
-
end
|
524
|
-
|
525
|
-
def respond_to_readable_server(server)
|
526
|
-
server.multi_response_nonblock.each_pair do |key, value_list|
|
527
|
-
yield @key_manager.key_without_namespace(key), value_list
|
528
|
-
end
|
529
|
-
|
530
|
-
server.multi_response_completed?
|
531
|
-
end
|
532
|
-
|
533
|
-
def servers_with_data(servers, timeout)
|
534
|
-
readable, = IO.select(servers.map(&:sock), nil, nil, timeout)
|
535
|
-
return [] if readable.nil?
|
536
|
-
|
537
|
-
readable.map(&:server)
|
538
|
-
end
|
539
|
-
|
540
|
-
def groups_for_keys(*keys)
|
541
|
-
keys.flatten!
|
542
|
-
keys.map! { |a| @key_manager.validate_key(a.to_s) }
|
543
|
-
ring.keys_grouped_by_server(keys)
|
409
|
+
def pipelined_getter
|
410
|
+
PipelinedGetter.new(ring, @key_manager)
|
544
411
|
end
|
545
412
|
end
|
546
413
|
end
|
data/lib/dalli/options.rb
CHANGED
@@ -31,19 +31,19 @@ module Dalli
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
def
|
34
|
+
def pipeline_response_start
|
35
35
|
@lock.synchronize do
|
36
36
|
super
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
def
|
40
|
+
def process_outstanding_pipeline_requests
|
41
41
|
@lock.synchronize do
|
42
42
|
super
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
def
|
46
|
+
def pipeline_response_abort
|
47
47
|
@lock.synchronize do
|
48
48
|
super
|
49
49
|
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
##
|
5
|
+
# Contains logic for the pipelined gets implemented by the client.
|
6
|
+
##
|
7
|
+
class PipelinedGetter
|
8
|
+
def initialize(ring, key_manager)
|
9
|
+
@ring = ring
|
10
|
+
@key_manager = key_manager
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Yields, one at a time, keys and their values+attributes.
|
15
|
+
#
|
16
|
+
def process(keys, &block)
|
17
|
+
return {} if keys.empty?
|
18
|
+
|
19
|
+
@ring.lock do
|
20
|
+
servers = setup_requests(keys)
|
21
|
+
start_time = Time.now
|
22
|
+
loop do
|
23
|
+
# Remove any servers which are not connected
|
24
|
+
servers.delete_if { |s| !s.connected? }
|
25
|
+
break if servers.empty?
|
26
|
+
|
27
|
+
servers = fetch_responses(servers, start_time, @ring.socket_timeout, &block)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
rescue NetworkError => e
|
31
|
+
Dalli.logger.debug { e.inspect }
|
32
|
+
Dalli.logger.debug { 'retrying pipelined gets because of timeout' }
|
33
|
+
retry
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_requests(keys)
|
37
|
+
groups = groups_for_keys(keys)
|
38
|
+
make_getkq_requests(groups)
|
39
|
+
|
40
|
+
# TODO: How does this exit on a NetworkError
|
41
|
+
finish_queries(groups.keys)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Loop through the server-grouped sets of keys, writing
|
46
|
+
# the corresponding getkq requests to the appropriate servers
|
47
|
+
##
|
48
|
+
def make_getkq_requests(groups)
|
49
|
+
groups.each do |server, keys_for_server|
|
50
|
+
server.request(:pipelined_get, keys_for_server)
|
51
|
+
rescue DalliError, NetworkError => e
|
52
|
+
Dalli.logger.debug { e.inspect }
|
53
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# This loops through the servers that have keys in
|
59
|
+
# our set, sending the noop to terminate the set of queries.
|
60
|
+
##
|
61
|
+
def finish_queries(servers)
|
62
|
+
deleted = []
|
63
|
+
|
64
|
+
servers.each do |server|
|
65
|
+
next unless server.alive?
|
66
|
+
|
67
|
+
begin
|
68
|
+
finish_query_for_server(server)
|
69
|
+
rescue Dalli::NetworkError
|
70
|
+
raise
|
71
|
+
rescue Dalli::DalliError
|
72
|
+
deleted.append(server)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
servers.delete_if { |server| deleted.include?(server) }
|
77
|
+
rescue Dalli::NetworkError
|
78
|
+
abort_without_timeout(servers)
|
79
|
+
raise
|
80
|
+
end
|
81
|
+
|
82
|
+
def finish_query_for_server(server)
|
83
|
+
server.pipeline_response_start
|
84
|
+
rescue Dalli::NetworkError
|
85
|
+
raise
|
86
|
+
rescue Dalli::DalliError => e
|
87
|
+
Dalli.logger.debug { e.inspect }
|
88
|
+
Dalli.logger.debug { "Results from server: #{server.name} will be missing from the results" }
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
|
92
|
+
# Swallows Dalli::NetworkError
|
93
|
+
def abort_without_timeout(servers)
|
94
|
+
servers.each(&:pipeline_response_abort)
|
95
|
+
end
|
96
|
+
|
97
|
+
def fetch_responses(servers, start_time, timeout, &block)
|
98
|
+
time_left = remaining_time(start_time, timeout)
|
99
|
+
readable_servers = servers_with_response(servers, time_left)
|
100
|
+
if readable_servers.empty?
|
101
|
+
abort_with_timeout(servers)
|
102
|
+
return []
|
103
|
+
end
|
104
|
+
|
105
|
+
# Loop through the servers with responses, and
|
106
|
+
# delete any from our list that are finished
|
107
|
+
readable_servers.each do |server|
|
108
|
+
servers.delete(server) if process_server(server, &block)
|
109
|
+
end
|
110
|
+
servers
|
111
|
+
rescue NetworkError
|
112
|
+
# Abort and raise if we encountered a network error. This triggers
|
113
|
+
# a retry at the top level.
|
114
|
+
abort_without_timeout(servers)
|
115
|
+
raise
|
116
|
+
end
|
117
|
+
|
118
|
+
def remaining_time(start, timeout)
|
119
|
+
elapsed = Time.now - start
|
120
|
+
return 0 if elapsed > timeout
|
121
|
+
|
122
|
+
timeout - elapsed
|
123
|
+
end
|
124
|
+
|
125
|
+
# Swallows Dalli::NetworkError
|
126
|
+
def abort_with_timeout(servers)
|
127
|
+
abort_without_timeout(servers)
|
128
|
+
servers.each do |server|
|
129
|
+
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
130
|
+
end
|
131
|
+
|
132
|
+
true # Required to simplify caller
|
133
|
+
end
|
134
|
+
|
135
|
+
# Processes responses from a server. Returns true if there are no
|
136
|
+
# additional responses from this server.
|
137
|
+
def process_server(server)
|
138
|
+
server.process_outstanding_pipeline_requests.each_pair do |key, value_list|
|
139
|
+
yield @key_manager.key_without_namespace(key), value_list
|
140
|
+
end
|
141
|
+
|
142
|
+
server.pipeline_response_completed?
|
143
|
+
end
|
144
|
+
|
145
|
+
def servers_with_response(servers, timeout)
|
146
|
+
return [] if servers.empty?
|
147
|
+
|
148
|
+
# TODO: - This is a bit challenging. Essentially the PipelinedGetter
|
149
|
+
# is a reactor, but without the benefit of a Fiber or separate thread.
|
150
|
+
# My suspicion is that we may want to try and push this down into the
|
151
|
+
# individual servers, but I'm not sure. For now, we keep the
|
152
|
+
# mapping between the alerted object (the socket) and the
|
153
|
+
# corrresponding server here.
|
154
|
+
server_map = servers.each_with_object({}) { |s, h| h[s.sock] = s }
|
155
|
+
|
156
|
+
readable, = IO.select(server_map.keys, nil, nil, timeout)
|
157
|
+
return [] if readable.nil?
|
158
|
+
|
159
|
+
readable.map { |sock| server_map[sock] }
|
160
|
+
end
|
161
|
+
|
162
|
+
def groups_for_keys(*keys)
|
163
|
+
keys.flatten!
|
164
|
+
keys.map! { |a| @key_manager.validate_key(a.to_s) }
|
165
|
+
groups = @ring.keys_grouped_by_server(keys)
|
166
|
+
if (unfound_keys = groups.delete(nil))
|
167
|
+
Dalli.logger.debug do
|
168
|
+
"unable to get keys for #{unfound_keys.length} keys "\
|
169
|
+
'because no matching server was found'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
groups
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dalli
|
4
|
+
module Protocol
|
5
|
+
class Binary
|
6
|
+
##
|
7
|
+
# Class that encapsulates data parsed from a memcached response header.
|
8
|
+
##
|
9
|
+
class ResponseHeader
|
10
|
+
SIZE = 24
|
11
|
+
FMT = '@2nCCnNNQ'
|
12
|
+
|
13
|
+
attr_reader :key_len, :extra_len, :data_type, :status, :body_len, :opaque, :cas
|
14
|
+
|
15
|
+
def initialize(buf)
|
16
|
+
raise ArgumentError, "Response buffer must be at least #{SIZE} bytes" unless buf.bytesize >= SIZE
|
17
|
+
|
18
|
+
@key_len, @extra_len, @data_type, @status, @body_len, @opaque, @cas = buf.unpack(FMT)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ok?
|
22
|
+
status.zero?
|
23
|
+
end
|
24
|
+
|
25
|
+
def not_found?
|
26
|
+
status == 1
|
27
|
+
end
|
28
|
+
|
29
|
+
NOT_STORED_STATUSES = [2, 5].freeze
|
30
|
+
def not_stored?
|
31
|
+
NOT_STORED_STATUSES.include?(status)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -9,9 +9,6 @@ module Dalli
|
|
9
9
|
# and parsing into local values. Handles errors on unexpected values.
|
10
10
|
##
|
11
11
|
class ResponseProcessor
|
12
|
-
RESP_HEADER = '@2nCCnNNQ'
|
13
|
-
RESP_HEADER_SIZE = 24
|
14
|
-
|
15
12
|
# Response codes taken from:
|
16
13
|
# https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
|
17
14
|
RESPONSE_CODES = {
|
@@ -44,14 +41,9 @@ module Dalli
|
|
44
41
|
end
|
45
42
|
|
46
43
|
def read_response
|
47
|
-
|
48
|
-
body = read(body_len) if body_len.positive?
|
49
|
-
[
|
50
|
-
end
|
51
|
-
|
52
|
-
def unpack_header(header)
|
53
|
-
(key_len, extra_len, _, status, body_len, _, cas) = header.unpack(RESP_HEADER)
|
54
|
-
[status, extra_len, key_len, body_len, cas]
|
44
|
+
resp_header = ResponseHeader.new(read_header)
|
45
|
+
body = read(resp_header.body_len) if resp_header.body_len.positive?
|
46
|
+
[resp_header, body]
|
55
47
|
end
|
56
48
|
|
57
49
|
def unpack_response_body(extra_len, key_len, body, unpack)
|
@@ -63,45 +55,36 @@ module Dalli
|
|
63
55
|
end
|
64
56
|
|
65
57
|
def read_header
|
66
|
-
read(
|
58
|
+
read(ResponseHeader::SIZE) || raise(Dalli::NetworkError, 'No response')
|
67
59
|
end
|
68
60
|
|
69
|
-
def
|
70
|
-
|
71
|
-
end
|
61
|
+
def raise_on_not_ok_status!(resp_header)
|
62
|
+
return if resp_header.ok?
|
72
63
|
|
73
|
-
|
74
|
-
def not_stored?(status)
|
75
|
-
NOT_STORED_STATUSES.include?(status)
|
76
|
-
end
|
77
|
-
|
78
|
-
def raise_on_not_ok_status!(status)
|
79
|
-
return if status.zero?
|
80
|
-
|
81
|
-
raise Dalli::DalliError, "Response error #{status}: #{RESPONSE_CODES[status]}"
|
64
|
+
raise Dalli::DalliError, "Response error #{resp_header.status}: #{RESPONSE_CODES[resp_header.status]}"
|
82
65
|
end
|
83
66
|
|
84
67
|
def generic_response(unpack: false, cache_nils: false)
|
85
|
-
|
68
|
+
resp_header, body = read_response
|
86
69
|
|
87
|
-
return cache_nils ? ::Dalli::NOT_FOUND : nil if not_found?
|
88
|
-
return false if not_stored?
|
70
|
+
return cache_nils ? ::Dalli::NOT_FOUND : nil if resp_header.not_found?
|
71
|
+
return false if resp_header.not_stored? # Not stored, normal status for add operation
|
89
72
|
|
90
|
-
raise_on_not_ok_status!(
|
73
|
+
raise_on_not_ok_status!(resp_header)
|
91
74
|
return true unless body
|
92
75
|
|
93
|
-
unpack_response_body(extra_len, key_len, body, unpack).last
|
76
|
+
unpack_response_body(resp_header.extra_len, resp_header.key_len, body, unpack).last
|
94
77
|
end
|
95
78
|
|
96
79
|
def data_cas_response
|
97
|
-
|
98
|
-
return [nil, cas] if not_found?
|
99
|
-
return [nil, false] if not_stored?
|
80
|
+
resp_header, body = read_response
|
81
|
+
return [nil, resp_header.cas] if resp_header.not_found?
|
82
|
+
return [nil, false] if resp_header.not_stored?
|
100
83
|
|
101
|
-
raise_on_not_ok_status!(
|
102
|
-
return [nil, cas] unless body
|
84
|
+
raise_on_not_ok_status!(resp_header)
|
85
|
+
return [nil, resp_header.cas] unless body
|
103
86
|
|
104
|
-
[unpack_response_body(extra_len, key_len, body, true).last, cas]
|
87
|
+
[unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true).last, resp_header.cas]
|
105
88
|
end
|
106
89
|
|
107
90
|
def cas_response
|
@@ -111,17 +94,17 @@ module Dalli
|
|
111
94
|
def multi_with_keys_response
|
112
95
|
hash = {}
|
113
96
|
loop do
|
114
|
-
|
97
|
+
resp_header, body = read_response
|
115
98
|
# This is the response to the terminating noop / end of stat
|
116
|
-
return hash if
|
99
|
+
return hash if resp_header.ok? && resp_header.key_len.zero?
|
117
100
|
|
118
101
|
# Ignore any responses with non-zero status codes,
|
119
102
|
# such as errors from set operations. That allows
|
120
103
|
# this code to be used at the end of a multi
|
121
104
|
# block to clear any error responses from inside the multi.
|
122
|
-
next unless
|
105
|
+
next unless resp_header.ok?
|
123
106
|
|
124
|
-
key, value = unpack_response_body(extra_len, key_len, body, true)
|
107
|
+
key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
|
125
108
|
hash[key] = value
|
126
109
|
end
|
127
110
|
end
|
@@ -137,11 +120,57 @@ module Dalli
|
|
137
120
|
raise Dalli::NetworkError, "Unexpected message format: #{extra_len} #{count}"
|
138
121
|
end
|
139
122
|
|
140
|
-
def auth_response
|
141
|
-
|
142
|
-
|
123
|
+
def auth_response(buf = read_header)
|
124
|
+
resp_header = ResponseHeader.new(buf)
|
125
|
+
body_len = resp_header.body_len
|
126
|
+
validate_auth_format(resp_header.extra_len, body_len)
|
143
127
|
content = read(body_len) if body_len.positive?
|
144
|
-
[status, content]
|
128
|
+
[resp_header.status, content]
|
129
|
+
end
|
130
|
+
|
131
|
+
def contains_header?(buf)
|
132
|
+
return false unless buf
|
133
|
+
|
134
|
+
buf.bytesize >= ResponseHeader::SIZE
|
135
|
+
end
|
136
|
+
|
137
|
+
def response_header_from_buffer(buf)
|
138
|
+
header = buf.slice(0, ResponseHeader::SIZE)
|
139
|
+
ResponseHeader.new(header)
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# This method returns an array of values used in a pipelined
|
144
|
+
# getk process. The first value is the number of bytes by
|
145
|
+
# which to advance the pointer in the buffer. If the
|
146
|
+
# complete response is found in the buffer, this will
|
147
|
+
# be the response size. Otherwise it is zero.
|
148
|
+
#
|
149
|
+
# The remaining four values in the array are the status, key,
|
150
|
+
# value, and cas returned from the response.
|
151
|
+
##
|
152
|
+
def getk_response_from_buffer(buf)
|
153
|
+
# There's no header in the buffer, so don't advance
|
154
|
+
return [0, 0, nil, nil, nil] unless contains_header?(buf)
|
155
|
+
|
156
|
+
resp_header = response_header_from_buffer(buf)
|
157
|
+
body_len = resp_header.body_len
|
158
|
+
|
159
|
+
# The response has no body - so we need to advance the
|
160
|
+
# buffer. This is either the response to the terminating
|
161
|
+
# noop or, if the status is not zero, an intermediate
|
162
|
+
# error response that needs to be discarded.
|
163
|
+
return [ResponseHeader::SIZE, resp_header.status, nil, nil, resp_header.cas] if body_len.zero?
|
164
|
+
|
165
|
+
# The header is in the buffer, but the body is not
|
166
|
+
resp_size = ResponseHeader::SIZE + body_len
|
167
|
+
return [0, resp_header.status, nil, nil, nil] unless buf.bytesize >= resp_size
|
168
|
+
|
169
|
+
# The full response is in our buffer, so parse it and return
|
170
|
+
# the values
|
171
|
+
body = buf.slice(ResponseHeader::SIZE, body_len)
|
172
|
+
key, value = unpack_response_body(resp_header.extra_len, resp_header.key_len, body, true)
|
173
|
+
[resp_size, resp_header.status, key, value, resp_header.cas]
|
145
174
|
end
|
146
175
|
end
|
147
176
|
end
|
@@ -6,6 +6,7 @@ require 'socket'
|
|
6
6
|
require 'timeout'
|
7
7
|
|
8
8
|
require_relative 'binary/request_formatter'
|
9
|
+
require_relative 'binary/response_header'
|
9
10
|
require_relative 'binary/response_processor'
|
10
11
|
require_relative 'binary/sasl_authentication'
|
11
12
|
|
@@ -32,9 +33,7 @@ module Dalli
|
|
32
33
|
# times a socket operation may fail before considering the server dead
|
33
34
|
socket_max_failures: 2,
|
34
35
|
# amount of time to sleep between retries when a failure occurs
|
35
|
-
socket_failure_delay: 0.1
|
36
|
-
username: nil,
|
37
|
-
password: nil
|
36
|
+
socket_failure_delay: 0.1
|
38
37
|
}.freeze
|
39
38
|
|
40
39
|
def initialize(attribs, options = {})
|
@@ -42,6 +41,7 @@ module Dalli
|
|
42
41
|
@options = DEFAULTS.merge(options)
|
43
42
|
@value_marshaller = ValueMarshaller.new(@options)
|
44
43
|
@response_processor = ResponseProcessor.new(self, @value_marshaller)
|
44
|
+
@response_buffer = ResponseBuffer.new(self, @response_processor)
|
45
45
|
|
46
46
|
reset_down_info
|
47
47
|
@sock = nil
|
@@ -49,6 +49,10 @@ module Dalli
|
|
49
49
|
@request_in_progress = false
|
50
50
|
end
|
51
51
|
|
52
|
+
def response_buffer
|
53
|
+
@response_buffer ||= ResponseBuffer.new(self, @response_processor)
|
54
|
+
end
|
55
|
+
|
52
56
|
def name
|
53
57
|
if socket_type == :unix
|
54
58
|
hostname
|
@@ -58,8 +62,8 @@ module Dalli
|
|
58
62
|
end
|
59
63
|
|
60
64
|
# Chokepoint method for error handling and ensuring liveness
|
61
|
-
def request(
|
62
|
-
verify_state
|
65
|
+
def request(opkey, *args)
|
66
|
+
verify_state(opkey)
|
63
67
|
# The alive? call has the side effect of connecting the underlying
|
64
68
|
# socket if it is not connected, or there's been a disconnect
|
65
69
|
# because of timeout or other error. Method raises an error
|
@@ -67,7 +71,7 @@ module Dalli
|
|
67
71
|
raise_memcached_down_err unless alive?
|
68
72
|
|
69
73
|
begin
|
70
|
-
send(
|
74
|
+
send(opkey, *args)
|
71
75
|
rescue Dalli::MarshalError => e
|
72
76
|
log_marshall_err(args.first, e)
|
73
77
|
raise
|
@@ -145,80 +149,79 @@ module Dalli
|
|
145
149
|
# flushing responses for kv pairs that were found.
|
146
150
|
#
|
147
151
|
# Returns nothing.
|
148
|
-
def
|
149
|
-
verify_state
|
152
|
+
def pipeline_response_start
|
153
|
+
verify_state(:getkq)
|
150
154
|
write_noop
|
151
|
-
|
152
|
-
@position = 0
|
155
|
+
response_buffer.reset
|
153
156
|
start_request!
|
154
157
|
end
|
155
158
|
|
156
|
-
# Did the last call to #
|
157
|
-
def
|
158
|
-
|
159
|
+
# Did the last call to #pipeline_response_start complete successfully?
|
160
|
+
def pipeline_response_completed?
|
161
|
+
response_buffer.completed?
|
162
|
+
end
|
163
|
+
|
164
|
+
def pipeline_response(bytes_to_advance = 0)
|
165
|
+
response_buffer.process_single_response(bytes_to_advance)
|
166
|
+
end
|
167
|
+
|
168
|
+
def reconnect_on_pipeline_complete!
|
169
|
+
reconnect! 'multi_response has completed' if pipeline_response_completed?
|
159
170
|
end
|
160
171
|
|
161
172
|
# Attempt to receive and parse as many key/value pairs as possible
|
162
|
-
# from this server. After #
|
173
|
+
# from this server. After #pipeline_response_start, this should be invoked
|
163
174
|
# repeatedly whenever this server's socket is readable until
|
164
|
-
# #
|
175
|
+
# #pipeline_response_completed?.
|
165
176
|
#
|
166
177
|
# Returns a Hash of kv pairs received.
|
167
|
-
def
|
168
|
-
|
169
|
-
|
170
|
-
@multi_buffer << @sock.read_available
|
171
|
-
buf = @multi_buffer
|
172
|
-
pos = @position
|
178
|
+
def process_outstanding_pipeline_requests
|
179
|
+
reconnect_on_pipeline_complete!
|
173
180
|
values = {}
|
174
181
|
|
175
|
-
|
176
|
-
header = buf.slice(pos, ResponseProcessor::RESP_HEADER_SIZE)
|
177
|
-
_, extra_len, key_len, body_len, cas = @response_processor.unpack_header(header)
|
182
|
+
response_buffer.read
|
178
183
|
|
179
|
-
|
180
|
-
|
181
|
-
|
184
|
+
bytes_to_advance, status, key, value, cas = pipeline_response
|
185
|
+
# Loop while we have at least a complete header in the buffer
|
186
|
+
while bytes_to_advance.positive?
|
187
|
+
# If the status and key length are both zero, then this is the response
|
188
|
+
# to the noop at the end of the pipeline
|
189
|
+
if status.zero? && key.nil?
|
190
|
+
finish_pipeline
|
182
191
|
break
|
183
192
|
end
|
184
193
|
|
185
|
-
#
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
body = buf.slice(pos + ResponseProcessor::RESP_HEADER_SIZE, body_len)
|
190
|
-
begin
|
191
|
-
key, value = @response_processor.unpack_response_body(extra_len, key_len, body, true)
|
192
|
-
values[key] = [value, cas]
|
193
|
-
rescue DalliError
|
194
|
-
# TODO: Determine if we should be swallowing
|
195
|
-
# this error
|
196
|
-
end
|
194
|
+
# If the status is zero and the key len is positive, then this is a
|
195
|
+
# getkq response with a value that we want to set in the response hash
|
196
|
+
values[key] = [value, cas] unless key.nil?
|
197
197
|
|
198
|
-
|
198
|
+
# Get the next set of bytes from the buffer
|
199
|
+
bytes_to_advance, status, key, value, cas = pipeline_response(bytes_to_advance)
|
199
200
|
end
|
200
|
-
# TODO: We should be discarding the already processed buffer at this point
|
201
|
-
@position = pos
|
202
201
|
|
203
202
|
values
|
204
203
|
rescue SystemCallError, Timeout::Error, EOFError => e
|
205
204
|
failure!(e)
|
206
205
|
end
|
207
206
|
|
208
|
-
def
|
209
|
-
@
|
210
|
-
|
207
|
+
def read_nonblock
|
208
|
+
@sock.read_available
|
209
|
+
end
|
210
|
+
|
211
|
+
# Called after the noop response is received at the end of a set
|
212
|
+
# of pipelined gets
|
213
|
+
def finish_pipeline
|
214
|
+
response_buffer.clear
|
211
215
|
finish_request!
|
212
216
|
end
|
213
217
|
|
214
|
-
# Abort an earlier #
|
218
|
+
# Abort an earlier #pipeline_response_start. Used to signal an external
|
215
219
|
# timeout. The underlying socket is disconnected, and the exception is
|
216
220
|
# swallowed.
|
217
221
|
#
|
218
222
|
# Returns nothing.
|
219
|
-
def
|
220
|
-
|
221
|
-
@position = nil
|
223
|
+
def pipeline_response_abort
|
224
|
+
response_buffer.clear
|
222
225
|
abort_request!
|
223
226
|
return true unless @sock
|
224
227
|
|
@@ -245,6 +248,10 @@ module Dalli
|
|
245
248
|
failure!(e)
|
246
249
|
end
|
247
250
|
|
251
|
+
def connected?
|
252
|
+
!@sock.nil?
|
253
|
+
end
|
254
|
+
|
248
255
|
def socket_timeout
|
249
256
|
@socket_timeout ||= @options[:socket_timeout]
|
250
257
|
end
|
@@ -269,15 +276,23 @@ module Dalli
|
|
269
276
|
@request_in_progress = false
|
270
277
|
end
|
271
278
|
|
272
|
-
def verify_state
|
279
|
+
def verify_state(opkey)
|
273
280
|
failure!(RuntimeError.new('Already writing to socket')) if request_in_progress?
|
274
281
|
reconnect_on_fork if fork_detected?
|
282
|
+
verify_allowed_multi!(opkey) if multi?
|
275
283
|
end
|
276
284
|
|
277
285
|
def fork_detected?
|
278
286
|
@pid && @pid != Process.pid
|
279
287
|
end
|
280
288
|
|
289
|
+
ALLOWED_MULTI_OPS = %i[add addq delete deleteq replace replaceq set setq noop].freeze
|
290
|
+
def verify_allowed_multi!(opkey)
|
291
|
+
return if ALLOWED_MULTI_OPS.include?(opkey)
|
292
|
+
|
293
|
+
raise Dalli::NotPermittedMultiOpError, "The operation #{opkey} is not allowed in a multi block."
|
294
|
+
end
|
295
|
+
|
281
296
|
def reconnect_on_fork
|
282
297
|
message = 'Fork detected, re-connecting child process...'
|
283
298
|
Dalli.logger.info { message }
|
@@ -351,7 +366,7 @@ module Dalli
|
|
351
366
|
end
|
352
367
|
|
353
368
|
def multi?
|
354
|
-
Thread.current[
|
369
|
+
Thread.current[::Dalli::MULTI_KEY]
|
355
370
|
end
|
356
371
|
|
357
372
|
def cache_nils?(opts)
|
@@ -366,12 +381,12 @@ module Dalli
|
|
366
381
|
@response_processor.generic_response(unpack: true, cache_nils: cache_nils?(options))
|
367
382
|
end
|
368
383
|
|
369
|
-
def
|
384
|
+
def pipelined_get(keys)
|
370
385
|
req = +''
|
371
386
|
keys.each do |key|
|
372
387
|
req << RequestFormatter.standard_request(opkey: :getkq, key: key)
|
373
388
|
end
|
374
|
-
# Could send noop here instead of in
|
389
|
+
# Could send noop here instead of in pipeline_response_start
|
375
390
|
write(req)
|
376
391
|
end
|
377
392
|
|
@@ -520,9 +535,9 @@ module Dalli
|
|
520
535
|
|
521
536
|
def memcached_socket
|
522
537
|
if socket_type == :unix
|
523
|
-
Dalli::Socket::UNIX.open(hostname,
|
538
|
+
Dalli::Socket::UNIX.open(hostname, options)
|
524
539
|
else
|
525
|
-
Dalli::Socket::TCP.open(hostname, port,
|
540
|
+
Dalli::Socket::TCP.open(hostname, port, options)
|
526
541
|
end
|
527
542
|
end
|
528
543
|
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
module Dalli
|
7
|
+
module Protocol
|
8
|
+
##
|
9
|
+
# Manages the buffer for responses from memcached.
|
10
|
+
##
|
11
|
+
class ResponseBuffer
|
12
|
+
def initialize(io_source, response_processor)
|
13
|
+
@io_source = io_source
|
14
|
+
@response_processor = response_processor
|
15
|
+
end
|
16
|
+
|
17
|
+
def read
|
18
|
+
@buffer << @io_source.read_nonblock
|
19
|
+
end
|
20
|
+
|
21
|
+
# Attempts to process a single response from the buffer. Starts
|
22
|
+
# by advancing the buffer to the specified start position
|
23
|
+
def process_single_response(start_position = 0)
|
24
|
+
advance(start_position)
|
25
|
+
@response_processor.getk_response_from_buffer(@buffer)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Advances the internal response buffer by bytes_to_advance
|
29
|
+
# bytes. The
|
30
|
+
def advance(bytes_to_advance)
|
31
|
+
@buffer = @buffer[bytes_to_advance..-1]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Resets the internal buffer to an empty state,
|
35
|
+
# so that we're ready to read pipelined responses
|
36
|
+
def reset
|
37
|
+
@buffer = +''
|
38
|
+
end
|
39
|
+
|
40
|
+
# Clear the internal response buffer
|
41
|
+
def clear
|
42
|
+
@buffer = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def completed?
|
46
|
+
@buffer.nil?
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/dalli/ring.rb
CHANGED
@@ -79,7 +79,7 @@ module Dalli
|
|
79
79
|
end
|
80
80
|
end
|
81
81
|
|
82
|
-
def
|
82
|
+
def pipeline_consume_and_ignore_responses
|
83
83
|
@servers.each do |s|
|
84
84
|
s.request(:noop)
|
85
85
|
rescue Dalli::NetworkError
|
@@ -92,6 +92,10 @@ module Dalli
|
|
92
92
|
@servers.first.socket_timeout
|
93
93
|
end
|
94
94
|
|
95
|
+
def close
|
96
|
+
@servers.each(&:close)
|
97
|
+
end
|
98
|
+
|
95
99
|
private
|
96
100
|
|
97
101
|
def threadsafe!
|
data/lib/dalli/socket.rb
CHANGED
@@ -85,13 +85,13 @@ module Dalli
|
|
85
85
|
##
|
86
86
|
class TCP < TCPSocket
|
87
87
|
include Dalli::Socket::InstanceMethods
|
88
|
-
|
88
|
+
# options - supports enhanced logging in the case of a timeout
|
89
|
+
attr_accessor :options
|
89
90
|
|
90
|
-
def self.open(host, port,
|
91
|
+
def self.open(host, port, options = {})
|
91
92
|
Timeout.timeout(options[:socket_timeout]) do
|
92
93
|
sock = new(host, port)
|
93
94
|
sock.options = { host: host, port: port }.merge(options)
|
94
|
-
sock.server = server
|
95
95
|
init_socket_options(sock, options)
|
96
96
|
|
97
97
|
options[:ssl_context] ? wrapping_ssl_socket(sock, host, options[:ssl_context]) : sock
|
@@ -132,13 +132,15 @@ module Dalli
|
|
132
132
|
##
|
133
133
|
class UNIX < UNIXSocket
|
134
134
|
include Dalli::Socket::InstanceMethods
|
135
|
-
attr_accessor :options, :server
|
136
135
|
|
137
|
-
|
136
|
+
# options - supports enhanced logging in the case of a timeout
|
137
|
+
# server - used to support IO.select in the pipelined getter
|
138
|
+
attr_accessor :options
|
139
|
+
|
140
|
+
def self.open(path, options = {})
|
138
141
|
Timeout.timeout(options[:socket_timeout]) do
|
139
142
|
sock = new(path)
|
140
143
|
sock.options = { path: path }.merge(options)
|
141
|
-
sock.server = server
|
142
144
|
sock
|
143
145
|
end
|
144
146
|
end
|
data/lib/dalli/version.rb
CHANGED
data/lib/dalli.rb
CHANGED
@@ -24,10 +24,15 @@ module Dalli
|
|
24
24
|
# payload too big for memcached
|
25
25
|
class ValueOverMaxSize < DalliError; end
|
26
26
|
|
27
|
+
# operation is not permitted in a multi block
|
28
|
+
class NotPermittedMultiOpError < DalliError; end
|
29
|
+
|
27
30
|
# Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
|
28
31
|
class NilObject; end # rubocop:disable Lint/EmptyClass
|
29
32
|
NOT_FOUND = NilObject.new
|
30
33
|
|
34
|
+
MULTI_KEY = :dalli_multi
|
35
|
+
|
31
36
|
def self.logger
|
32
37
|
@logger ||= (rails_logger || default_logger)
|
33
38
|
end
|
@@ -54,9 +59,11 @@ require_relative 'dalli/version'
|
|
54
59
|
require_relative 'dalli/compressor'
|
55
60
|
require_relative 'dalli/client'
|
56
61
|
require_relative 'dalli/key_manager'
|
62
|
+
require_relative 'dalli/pipelined_getter'
|
57
63
|
require_relative 'dalli/ring'
|
58
64
|
require_relative 'dalli/protocol'
|
59
65
|
require_relative 'dalli/protocol/binary'
|
66
|
+
require_relative 'dalli/protocol/response_buffer'
|
60
67
|
require_relative 'dalli/protocol/server_config_parser'
|
61
68
|
require_relative 'dalli/protocol/ttl_sanitizer'
|
62
69
|
require_relative 'dalli/protocol/value_compressor'
|
data/lib/rack/session/dalli.rb
CHANGED
@@ -8,14 +8,13 @@ require 'English'
|
|
8
8
|
module Rack
|
9
9
|
module Session
|
10
10
|
# Rack::Session::Dalli provides memcached based session management.
|
11
|
-
class Dalli < Abstract::
|
12
|
-
attr_reader :
|
11
|
+
class Dalli < Abstract::PersistedSecure
|
12
|
+
attr_reader :data
|
13
13
|
|
14
14
|
# Don't freeze this until we fix the specs/implementation
|
15
15
|
# rubocop:disable Style/MutableConstant
|
16
16
|
DEFAULT_DALLI_OPTIONS = {
|
17
|
-
namespace: 'rack:session'
|
18
|
-
memcache_server: 'localhost:11211'
|
17
|
+
namespace: 'rack:session'
|
19
18
|
}
|
20
19
|
# rubocop:enable Style/MutableConstant
|
21
20
|
|
@@ -33,25 +32,14 @@ module Rack
|
|
33
32
|
# ENV['MEMCACHE_SERVERS'] and use that value if it is available, or fall
|
34
33
|
# back to the same default behavior described above.
|
35
34
|
#
|
36
|
-
# Rack::Session::Dalli
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
# Dalli::Client (or ConnectionPool) and use that instead of letting
|
41
|
-
# Rack::Session::Dalli instantiate it on your behalf, simply pass it in
|
42
|
-
# as the `:cache` option. Please note that you will be responsible for
|
43
|
-
# setting the namespace and any other options on Dalli::Client.
|
35
|
+
# Rack::Session::Dalli accepts the same options as Dalli::Client, so
|
36
|
+
# it's worth reviewing its documentation. Perhaps most importantly,
|
37
|
+
# if you don't specify a `:namespace` option, Rack::Session::Dalli
|
38
|
+
# will default to using 'rack:session'.
|
44
39
|
#
|
45
|
-
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
# `:namespace` option, Rack::Session::Dalli will default to using
|
49
|
-
# "rack:session".
|
50
|
-
#
|
51
|
-
# Whether you are using the `:cache` option or not, it is not recommend
|
52
|
-
# to set `:expires_in`. Instead, use `:expire_after`, which will control
|
53
|
-
# both the expiration of the client cookie as well as the expiration of
|
54
|
-
# the corresponding entry in memcached.
|
40
|
+
# It is not recommended to set `:expires_in`. Instead, use `:expire_after`,
|
41
|
+
# which will control both the expiration of the client cookie as well
|
42
|
+
# as the expiration of the corresponding entry in memcached.
|
55
43
|
#
|
56
44
|
# Rack::Session::Dalli also accepts a host of options that control how
|
57
45
|
# the sessions and session cookies are managed, including the
|
@@ -78,87 +66,108 @@ module Rack
|
|
78
66
|
super
|
79
67
|
|
80
68
|
# Determine the default TTL for newly-created sessions
|
81
|
-
@default_ttl = ttl
|
82
|
-
|
83
|
-
# Normalize and validate passed options
|
84
|
-
mserv, mopts, popts = extract_dalli_options(options)
|
85
|
-
|
86
|
-
@pool = ConnectionPool.new(popts || {}) { ::Dalli::Client.new(mserv, mopts) }
|
69
|
+
@default_ttl = ttl(@default_options[:expire_after])
|
70
|
+
@data = build_data_source(options)
|
87
71
|
end
|
88
72
|
|
89
|
-
def
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
unless dc.add(sid, session, @default_ttl)
|
96
|
-
sid = old_sid
|
97
|
-
redo # generate a new sid and try again
|
98
|
-
end
|
99
|
-
end
|
100
|
-
[sid, session]
|
73
|
+
def find_session(_req, sid)
|
74
|
+
with_dalli_client([nil, {}]) do |dc|
|
75
|
+
existing_session = existing_session_for_sid(dc, sid)
|
76
|
+
return [sid, existing_session] unless existing_session.nil?
|
77
|
+
|
78
|
+
[create_sid_with_empty_session(dc), {}]
|
101
79
|
end
|
102
80
|
end
|
103
81
|
|
104
|
-
def
|
105
|
-
return false unless
|
82
|
+
def write_session(_req, sid, session, options)
|
83
|
+
return false unless sid
|
106
84
|
|
107
|
-
|
108
|
-
dc.set(
|
109
|
-
|
85
|
+
with_dalli_client(false) do |dc|
|
86
|
+
dc.set(memcached_key_from_sid(sid), session, ttl(options[:expire_after]))
|
87
|
+
sid
|
110
88
|
end
|
111
89
|
end
|
112
90
|
|
113
|
-
def
|
114
|
-
|
115
|
-
dc.delete(
|
91
|
+
def delete_session(_req, sid, options)
|
92
|
+
with_dalli_client do |dc|
|
93
|
+
dc.delete(memcached_key_from_sid(sid))
|
116
94
|
generate_sid_with(dc) unless options[:drop]
|
117
95
|
end
|
118
96
|
end
|
119
97
|
|
120
|
-
|
121
|
-
|
98
|
+
private
|
99
|
+
|
100
|
+
def memcached_key_from_sid(sid)
|
101
|
+
sid.private_id
|
122
102
|
end
|
123
103
|
|
124
|
-
def
|
125
|
-
|
104
|
+
def existing_session_for_sid(client, sid)
|
105
|
+
return nil unless sid && !sid.empty?
|
106
|
+
|
107
|
+
client.get(memcached_key_from_sid(sid))
|
126
108
|
end
|
127
109
|
|
128
|
-
def
|
129
|
-
|
110
|
+
def create_sid_with_empty_session(client)
|
111
|
+
loop do
|
112
|
+
sid = generate_sid_with(client)
|
113
|
+
|
114
|
+
break sid if client.add(memcached_key_from_sid(sid), {}, @default_ttl)
|
115
|
+
end
|
130
116
|
end
|
131
117
|
|
132
|
-
|
118
|
+
def generate_sid_with(client)
|
119
|
+
loop do
|
120
|
+
raw_sid = generate_sid
|
121
|
+
sid = raw_sid.is_a?(String) ? Rack::Session::SessionId.new(raw_sid) : raw_sid
|
122
|
+
break sid unless client.get(memcached_key_from_sid(sid))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_data_source(options)
|
127
|
+
server_configurations, client_options, pool_options = extract_dalli_options(options)
|
128
|
+
|
129
|
+
if pool_options.empty?
|
130
|
+
::Dalli::Client.new(server_configurations, client_options)
|
131
|
+
else
|
132
|
+
ensure_connection_pool_added!
|
133
|
+
ConnectionPool.new(pool_options) do
|
134
|
+
::Dalli::Client.new(server_configurations, client_options.merge(threadsafe: false))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
133
138
|
|
134
139
|
def extract_dalli_options(options)
|
135
140
|
raise 'Rack::Session::Dalli no longer supports the :cache option.' if options[:cache]
|
136
141
|
|
137
|
-
|
142
|
+
client_options = retrieve_client_options(options)
|
143
|
+
server_configurations = client_options.delete(:memcache_server)
|
144
|
+
|
145
|
+
[server_configurations, client_options, retrieve_pool_options(options)]
|
146
|
+
end
|
147
|
+
|
148
|
+
def retrieve_client_options(options)
|
138
149
|
# Filter out Rack::Session-specific options and apply our defaults
|
139
150
|
filtered_opts = options.reject { |k, _| DEFAULT_OPTIONS.key? k }
|
140
|
-
|
141
|
-
mserv = mopts.delete :memcache_server
|
142
|
-
|
143
|
-
popts = {}
|
144
|
-
if mopts[:pool_size] || mopts[:pool_timeout]
|
145
|
-
popts[:size] = mopts.delete :pool_size if mopts[:pool_size]
|
146
|
-
popts[:timeout] = mopts.delete :pool_timeout if mopts[:pool_timeout]
|
147
|
-
mopts[:threadsafe] = true
|
148
|
-
end
|
149
|
-
|
150
|
-
[mserv, mopts, popts]
|
151
|
+
DEFAULT_DALLI_OPTIONS.merge(filtered_opts)
|
151
152
|
end
|
152
153
|
|
153
|
-
def
|
154
|
-
|
155
|
-
|
156
|
-
|
154
|
+
def retrieve_pool_options(options)
|
155
|
+
{}.tap do |pool_options|
|
156
|
+
pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
|
157
|
+
pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
|
157
158
|
end
|
158
159
|
end
|
159
160
|
|
160
|
-
def
|
161
|
-
|
161
|
+
def ensure_connection_pool_added!
|
162
|
+
require 'connection_pool'
|
163
|
+
rescue LoadError => e
|
164
|
+
warn "You don't have connection_pool installed in your application. "\
|
165
|
+
'Please add it to your Gemfile and run bundle install'
|
166
|
+
raise e
|
167
|
+
end
|
168
|
+
|
169
|
+
def with_dalli_client(result_on_error = nil, &block)
|
170
|
+
@data.with(&block)
|
162
171
|
rescue ::Dalli::DalliError, Errno::ECONNREFUSED
|
163
172
|
raise if /undefined class/.match?($ERROR_INFO.message)
|
164
173
|
|
@@ -166,7 +175,7 @@ module Rack
|
|
166
175
|
warn "#{self} is unable to find memcached server."
|
167
176
|
warn $ERROR_INFO.inspect
|
168
177
|
end
|
169
|
-
|
178
|
+
result_on_error
|
170
179
|
end
|
171
180
|
|
172
181
|
def ttl(expire_after)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dalli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter M. Goldstein
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-12-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: connection_pool
|
@@ -29,16 +29,22 @@ dependencies:
|
|
29
29
|
name: rack
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '2.0'
|
32
35
|
- - ">="
|
33
36
|
- !ruby/object:Gem::Version
|
34
|
-
version:
|
37
|
+
version: 2.2.0
|
35
38
|
type: :development
|
36
39
|
prerelease: false
|
37
40
|
version_requirements: !ruby/object:Gem::Requirement
|
38
41
|
requirements:
|
42
|
+
- - "~>"
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '2.0'
|
39
45
|
- - ">="
|
40
46
|
- !ruby/object:Gem::Version
|
41
|
-
version:
|
47
|
+
version: 2.2.0
|
42
48
|
- !ruby/object:Gem::Dependency
|
43
49
|
name: rubocop
|
44
50
|
requirement: !ruby/object:Gem::Requirement
|
@@ -113,11 +119,14 @@ files:
|
|
113
119
|
- lib/dalli/compressor.rb
|
114
120
|
- lib/dalli/key_manager.rb
|
115
121
|
- lib/dalli/options.rb
|
122
|
+
- lib/dalli/pipelined_getter.rb
|
116
123
|
- lib/dalli/protocol.rb
|
117
124
|
- lib/dalli/protocol/binary.rb
|
118
125
|
- lib/dalli/protocol/binary/request_formatter.rb
|
126
|
+
- lib/dalli/protocol/binary/response_header.rb
|
119
127
|
- lib/dalli/protocol/binary/response_processor.rb
|
120
128
|
- lib/dalli/protocol/binary/sasl_authentication.rb
|
129
|
+
- lib/dalli/protocol/response_buffer.rb
|
121
130
|
- lib/dalli/protocol/server_config_parser.rb
|
122
131
|
- lib/dalli/protocol/ttl_sanitizer.rb
|
123
132
|
- lib/dalli/protocol/value_compressor.rb
|