dalli 5.0.5 → 5.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +172 -0
- data/README.md +6 -1
- data/lib/dalli/client.rb +274 -48
- data/lib/dalli/options.rb +1 -1
- data/lib/dalli/pipelined_deleter.rb +41 -17
- data/lib/dalli/pipelined_getter.rb +57 -8
- data/lib/dalli/pipelined_setter.rb +15 -3
- data/lib/dalli/protocol/base.rb +62 -5
- data/lib/dalli/protocol/connection_manager.rb +34 -10
- data/lib/dalli/protocol/key_regularizer.rb +25 -9
- data/lib/dalli/protocol/meta.rb +100 -90
- data/lib/dalli/protocol/request_formatter.rb +166 -27
- data/lib/dalli/protocol/response_buffer.rb +21 -22
- data/lib/dalli/protocol/response_processor.rb +64 -17
- data/lib/dalli/socket.rb +47 -32
- data/lib/dalli/version.rb +2 -2
- metadata +2 -3
- data/lib/dalli/pid_cache.rb +0 -40
|
@@ -20,13 +20,18 @@ module Dalli
|
|
|
20
20
|
##
|
|
21
21
|
# Yields, one at a time, keys and their values+attributes.
|
|
22
22
|
#
|
|
23
|
-
|
|
23
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
24
|
+
#
|
|
25
|
+
# A transient network error is retried automatically. If a server remains
|
|
26
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
27
|
+
#
|
|
28
|
+
def process(keys, req_options = nil, &block)
|
|
24
29
|
return {} if keys.empty?
|
|
25
30
|
|
|
26
31
|
@ring.lock do
|
|
27
32
|
# Stores partial results collected during interleaved send phase
|
|
28
33
|
@partial_results = {}
|
|
29
|
-
servers = setup_requests(keys)
|
|
34
|
+
servers = setup_requests(keys, req_options)
|
|
30
35
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
36
|
|
|
32
37
|
# First yield any partial results collected during interleaved send
|
|
@@ -40,6 +45,40 @@ module Dalli
|
|
|
40
45
|
retry
|
|
41
46
|
end
|
|
42
47
|
|
|
48
|
+
##
|
|
49
|
+
# Stale-aware bulk get across servers. Returns { key => metadata Hash } for
|
|
50
|
+
# the keys that were found; see Protocol::Meta#read_multi_with_metadata_req
|
|
51
|
+
# for why misses are absent rather than present with miss: true.
|
|
52
|
+
#
|
|
53
|
+
# Unlike #process this issues one request per server and reads its full
|
|
54
|
+
# response before moving on, rather than pipelining across servers: the
|
|
55
|
+
# metadata path has no interleaving support, and the stale-aware callers it
|
|
56
|
+
# serves fetch far smaller batches than get_multi does.
|
|
57
|
+
#
|
|
58
|
+
# `req_options` accepts :p_token/:l_token, applied to every key in the batch.
|
|
59
|
+
#
|
|
60
|
+
def process_with_metadata(keys, req_options = nil)
|
|
61
|
+
return {} if keys.empty?
|
|
62
|
+
|
|
63
|
+
@ring.lock do
|
|
64
|
+
results = {}
|
|
65
|
+
groups_for_keys(keys).each do |server, keys_for_server|
|
|
66
|
+
results.merge!(server.request(:read_multi_with_metadata_req, keys_for_server, req_options))
|
|
67
|
+
rescue Dalli::RetryableNetworkError
|
|
68
|
+
raise
|
|
69
|
+
rescue DalliError, NetworkError => e
|
|
70
|
+
Dalli.logger.debug { e.inspect }
|
|
71
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
72
|
+
end
|
|
73
|
+
results.transform_keys! { |key| @key_manager.key_without_namespace(key) }
|
|
74
|
+
results
|
|
75
|
+
end
|
|
76
|
+
rescue Dalli::RetryableNetworkError => e
|
|
77
|
+
Dalli.logger.debug { e.inspect }
|
|
78
|
+
Dalli.logger.debug { 'retrying pipelined get with metadata because of network error' }
|
|
79
|
+
retry
|
|
80
|
+
end
|
|
81
|
+
|
|
43
82
|
private
|
|
44
83
|
|
|
45
84
|
def yield_partial_results
|
|
@@ -49,9 +88,9 @@ module Dalli
|
|
|
49
88
|
@partial_results.clear
|
|
50
89
|
end
|
|
51
90
|
|
|
52
|
-
def setup_requests(keys)
|
|
91
|
+
def setup_requests(keys, req_options = nil)
|
|
53
92
|
groups = groups_for_keys(keys)
|
|
54
|
-
make_getkq_requests(groups)
|
|
93
|
+
make_getkq_requests(groups, req_options)
|
|
55
94
|
|
|
56
95
|
# TODO: How does this exit on a NetworkError
|
|
57
96
|
finish_queries(groups.keys)
|
|
@@ -65,17 +104,27 @@ module Dalli
|
|
|
65
104
|
# on the wire by switching from getkq to getq, and using
|
|
66
105
|
# the opaque value to match requests to responses.
|
|
67
106
|
##
|
|
68
|
-
def make_getkq_requests(groups)
|
|
107
|
+
def make_getkq_requests(groups, req_options = nil)
|
|
69
108
|
groups.each do |server, keys_for_server|
|
|
70
109
|
if keys_for_server.size <= INTERLEAVE_THRESHOLD
|
|
71
110
|
# Small batch - send all at once (existing behavior)
|
|
72
|
-
server.request(:pipelined_get, keys_for_server)
|
|
111
|
+
server.request(:pipelined_get, keys_for_server, req_options)
|
|
73
112
|
else
|
|
74
113
|
# Large batch - interleave sends with response draining
|
|
75
114
|
# Pass @partial_results directly to avoid hash allocation/merge overhead
|
|
76
|
-
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results)
|
|
115
|
+
server.request(:pipelined_get_interleaved, keys_for_server, CHUNK_SIZE, @partial_results, req_options)
|
|
77
116
|
end
|
|
78
|
-
|
|
117
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate:
|
|
118
|
+
# #process's top-level rescue retries the whole pipelined get on it. This
|
|
119
|
+
# rescue used to catch DalliError and NetworkError together -- since
|
|
120
|
+
# NetworkError < DalliError, that silently swallowed RetryableNetworkError
|
|
121
|
+
# too, dropping this server's keys from the result on a transient hiccup
|
|
122
|
+
# instead of retrying, with nothing surfaced above debug-level logging.
|
|
123
|
+
# Only a non-network DalliError (this server genuinely can't serve these
|
|
124
|
+
# keys) should be swallowed here.
|
|
125
|
+
rescue Dalli::NetworkError
|
|
126
|
+
raise
|
|
127
|
+
rescue DalliError => e
|
|
79
128
|
Dalli.logger.debug { e.inspect }
|
|
80
129
|
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
|
81
130
|
end
|
|
@@ -14,7 +14,9 @@ module Dalli
|
|
|
14
14
|
|
|
15
15
|
##
|
|
16
16
|
# Writes multiple key-value pairs to memcached.
|
|
17
|
-
#
|
|
17
|
+
#
|
|
18
|
+
# A transient network error is retried automatically. If a server remains
|
|
19
|
+
# unreachable after retrying, raises Dalli::NetworkError.
|
|
18
20
|
#
|
|
19
21
|
# @param hash [Hash] key-value pairs to set
|
|
20
22
|
# @param ttl [Integer] time-to-live in seconds
|
|
@@ -46,13 +48,21 @@ module Dalli
|
|
|
46
48
|
# Loop through the server-grouped sets of keys, writing
|
|
47
49
|
# the corresponding quiet set requests to the appropriate servers
|
|
48
50
|
##
|
|
51
|
+
# NetworkError (which RetryableNetworkError subclasses) must propagate: the
|
|
52
|
+
# top-level rescue in #process retries the whole pipelined set on it. A
|
|
53
|
+
# combined `rescue DalliError, NetworkError` would silently swallow
|
|
54
|
+
# RetryableNetworkError too -- since NetworkError < DalliError -- dropping
|
|
55
|
+
# this server's keys on a transient hiccup instead of retrying. Only a
|
|
56
|
+
# non-network DalliError should be swallowed here.
|
|
49
57
|
def make_set_requests(groups, hash, ttl, req_options)
|
|
50
58
|
groups.each do |server, keys_for_server|
|
|
51
59
|
keys_for_server.each do |key|
|
|
52
60
|
original_key = @key_manager.key_without_namespace(key)
|
|
53
61
|
value = hash[original_key]
|
|
54
62
|
server.request(:pipelined_set, key, value, ttl, req_options)
|
|
55
|
-
rescue
|
|
63
|
+
rescue Dalli::NetworkError
|
|
64
|
+
raise
|
|
65
|
+
rescue DalliError => e
|
|
56
66
|
Dalli.logger.debug { e.inspect }
|
|
57
67
|
Dalli.logger.debug { "unable to set key #{key} for server #{server.name}" }
|
|
58
68
|
end
|
|
@@ -65,7 +75,9 @@ module Dalli
|
|
|
65
75
|
def finish_requests(servers)
|
|
66
76
|
servers.each do |server|
|
|
67
77
|
server.request(:noop)
|
|
68
|
-
rescue
|
|
78
|
+
rescue Dalli::NetworkError
|
|
79
|
+
raise
|
|
80
|
+
rescue DalliError => e
|
|
69
81
|
Dalli.logger.debug { e.inspect }
|
|
70
82
|
Dalli.logger.debug { "unable to complete pipelined set on server #{server.name}" }
|
|
71
83
|
end
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -40,12 +40,14 @@ module Dalli
|
|
|
40
40
|
verify_state(opkey)
|
|
41
41
|
|
|
42
42
|
begin
|
|
43
|
+
request_completed = false
|
|
43
44
|
@connection_manager.start_request!
|
|
44
45
|
response = send(opkey, *args)
|
|
45
46
|
|
|
46
47
|
# pipelined_get/pipelined_get_interleaved emit query but don't read the response(s)
|
|
47
48
|
@connection_manager.finish_request! unless %i[pipelined_get pipelined_get_interleaved].include?(opkey)
|
|
48
49
|
|
|
50
|
+
request_completed = true
|
|
49
51
|
response
|
|
50
52
|
rescue Dalli::MarshalError => e
|
|
51
53
|
log_marshal_err(args.first, e)
|
|
@@ -56,6 +58,18 @@ module Dalli
|
|
|
56
58
|
log_unexpected_err(e)
|
|
57
59
|
close
|
|
58
60
|
raise
|
|
61
|
+
ensure
|
|
62
|
+
# If the begin block didn't complete -- any exception, including a
|
|
63
|
+
# non-StandardError such as Async::Stop or Thread#kill, which the
|
|
64
|
+
# rescue clauses above never see -- tear down the connection so a
|
|
65
|
+
# half-used client isn't returned to the pool.
|
|
66
|
+
#
|
|
67
|
+
# The local flag is deliberate: reading $ERROR_INFO here would see
|
|
68
|
+
# the *outer* exception when `request` is called from inside a
|
|
69
|
+
# rescue clause (a common cache-fallback shape), and would then
|
|
70
|
+
# falsely tear down the pipelined_get happy path, which leaves
|
|
71
|
+
# @request_in_progress true on purpose until the caller drains.
|
|
72
|
+
close unless request_completed
|
|
59
73
|
end
|
|
60
74
|
end
|
|
61
75
|
|
|
@@ -64,6 +78,19 @@ module Dalli
|
|
|
64
78
|
# particular memcached instance is available for use.
|
|
65
79
|
def alive?
|
|
66
80
|
ensure_connected!
|
|
81
|
+
rescue Dalli::RetryableNetworkError => e
|
|
82
|
+
# A single connection attempt failure is retryable -- the same
|
|
83
|
+
# contract #request enforces on the send/receive path. Retrying here
|
|
84
|
+
# lets error_on_request!'s own fail-count threshold decide when to
|
|
85
|
+
# give up: it keeps raising RetryableNetworkError until
|
|
86
|
+
# socket_max_failures is reached, then raises a terminal
|
|
87
|
+
# NetworkError via down!, which the next rescue converts to false.
|
|
88
|
+
# Without this, a single transient hiccup during the liveness check
|
|
89
|
+
# itself -- as opposed to an actual request -- would report this
|
|
90
|
+
# server as not alive even though it would have reconnected fine.
|
|
91
|
+
Dalli.logger.debug { e.inspect }
|
|
92
|
+
Dalli.logger.debug { "retrying connection attempt to #{name} because of network error" }
|
|
93
|
+
retry
|
|
67
94
|
rescue Dalli::NetworkError
|
|
68
95
|
# ensure_connected! raises a NetworkError if connection fails. We
|
|
69
96
|
# want to capture that error and convert it to a boolean value here.
|
|
@@ -95,7 +122,6 @@ module Dalli
|
|
|
95
122
|
# When a block is given, yields (key, value, cas) for each response,
|
|
96
123
|
# avoiding intermediate Hash allocation. Returns nil.
|
|
97
124
|
# Without a block, returns a Hash of { key => [value, cas] }.
|
|
98
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
99
125
|
def pipeline_next_responses(&block)
|
|
100
126
|
reconnect_on_pipeline_complete!
|
|
101
127
|
values = nil
|
|
@@ -224,13 +250,44 @@ module Dalli
|
|
|
224
250
|
opts[:cache_nils] ? true : false
|
|
225
251
|
end
|
|
226
252
|
|
|
253
|
+
# Extracts opaque routing-token kwargs (:p_token, :l_token) from a
|
|
254
|
+
# request-options Hash so they can be splatted into a RequestFormatter
|
|
255
|
+
# call. Returns {} when neither is set, so the splat is a no-op on the
|
|
256
|
+
# common path. Validation (type, forbidden bytes) happens at the
|
|
257
|
+
# wire-formatter level, where it can raise uniformly regardless of how
|
|
258
|
+
# the token reached the formatter.
|
|
259
|
+
def routing_token_kwargs(opts)
|
|
260
|
+
return {} unless opts.is_a?(Hash)
|
|
261
|
+
return {} unless opts[:p_token] || opts[:l_token]
|
|
262
|
+
|
|
263
|
+
{ p_token: opts[:p_token], l_token: opts[:l_token] }
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Maps the client-facing meta-delete options onto RequestFormatter's flag
|
|
267
|
+
# names, so they can be splatted into a meta_delete call. Returns {} when
|
|
268
|
+
# none are set, keeping the splat a no-op on the common path.
|
|
269
|
+
#
|
|
270
|
+
# :tombstone_ttl becomes the T flag, which is the same TTL the formatter
|
|
271
|
+
# already accepted -- deliberately not a second TTL parameter, since two
|
|
272
|
+
# would allow emitting two T tokens in one request. It is sanitized like
|
|
273
|
+
# every other TTL Dalli sends.
|
|
274
|
+
def tombstone_kwargs(opts)
|
|
275
|
+
return {} unless opts.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
kwargs = {}
|
|
278
|
+
kwargs[:stale] = true if opts[:invalidate]
|
|
279
|
+
kwargs[:ttl] = TtlSanitizer.sanitize(Integer(opts[:tombstone_ttl])) if opts[:tombstone_ttl]
|
|
280
|
+
kwargs[:drop_value] = true if opts[:drop_value]
|
|
281
|
+
kwargs
|
|
282
|
+
end
|
|
283
|
+
|
|
227
284
|
def connect
|
|
228
285
|
@connection_manager.establish_connection
|
|
229
286
|
@version = version
|
|
230
287
|
up!
|
|
231
288
|
end
|
|
232
289
|
|
|
233
|
-
def pipelined_get(keys)
|
|
290
|
+
def pipelined_get(keys, options = nil)
|
|
234
291
|
# Clear buffer to remove any stale data from interrupted operations.
|
|
235
292
|
# Use clear (not reset) to keep pipeline_complete? = true, which is
|
|
236
293
|
# the expected state before pipeline_response_setup is called.
|
|
@@ -238,7 +295,7 @@ module Dalli
|
|
|
238
295
|
|
|
239
296
|
req = +''
|
|
240
297
|
keys.each do |key|
|
|
241
|
-
req << quiet_get_request(key)
|
|
298
|
+
req << quiet_get_request(key, options)
|
|
242
299
|
end
|
|
243
300
|
# Could send noop here instead of in pipeline_response_setup
|
|
244
301
|
write(req)
|
|
@@ -247,7 +304,7 @@ module Dalli
|
|
|
247
304
|
# For large batches, interleave writing requests with draining responses.
|
|
248
305
|
# This prevents socket buffer deadlock when sending many keys.
|
|
249
306
|
# Populates the provided results hash with any responses drained during send.
|
|
250
|
-
def pipelined_get_interleaved(keys, chunk_size, results)
|
|
307
|
+
def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
|
|
251
308
|
# Initialize the response buffer for draining during send phase
|
|
252
309
|
response_buffer.ensure_ready
|
|
253
310
|
|
|
@@ -255,7 +312,7 @@ module Dalli
|
|
|
255
312
|
# Build and write this chunk of requests
|
|
256
313
|
req = +''
|
|
257
314
|
chunk.each do |key|
|
|
258
|
-
req << quiet_get_request(key)
|
|
315
|
+
req << quiet_get_request(key, options)
|
|
259
316
|
end
|
|
260
317
|
write(req)
|
|
261
318
|
@connection_manager.flush
|
|
@@ -4,8 +4,6 @@ require 'English'
|
|
|
4
4
|
require 'socket'
|
|
5
5
|
require 'timeout'
|
|
6
6
|
|
|
7
|
-
require 'dalli/pid_cache'
|
|
8
|
-
|
|
9
7
|
module Dalli
|
|
10
8
|
module Protocol
|
|
11
9
|
##
|
|
@@ -54,7 +52,7 @@ module Dalli
|
|
|
54
52
|
|
|
55
53
|
@sock = memcached_socket
|
|
56
54
|
@sock.sync = false # Enable buffered I/O for better performance
|
|
57
|
-
@pid =
|
|
55
|
+
@pid = Process.pid
|
|
58
56
|
@request_in_progress = false
|
|
59
57
|
rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
|
|
60
58
|
# SocketError = DNS resolution failure
|
|
@@ -117,10 +115,16 @@ module Dalli
|
|
|
117
115
|
@sock.close
|
|
118
116
|
rescue StandardError
|
|
119
117
|
nil
|
|
118
|
+
ensure
|
|
119
|
+
# A non-StandardError (e.g. a second Async::Stop fired into the
|
|
120
|
+
# fiber while it is already inside this cleanup) can escape
|
|
121
|
+
# @sock.close; run the state cleanup unconditionally so the client
|
|
122
|
+
# isn't returned to the pool with a half-closed socket and
|
|
123
|
+
# @request_in_progress == true.
|
|
124
|
+
@sock = nil
|
|
125
|
+
@pid = nil
|
|
126
|
+
abort_request!
|
|
120
127
|
end
|
|
121
|
-
@sock = nil
|
|
122
|
-
@pid = nil
|
|
123
|
-
abort_request!
|
|
124
128
|
end
|
|
125
129
|
|
|
126
130
|
def connected?
|
|
@@ -165,12 +169,16 @@ module Dalli
|
|
|
165
169
|
end
|
|
166
170
|
else
|
|
167
171
|
def read(count)
|
|
168
|
-
|
|
172
|
+
read_bytes(count)
|
|
169
173
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
|
|
170
174
|
error_on_request!(e)
|
|
171
175
|
end
|
|
172
176
|
end
|
|
173
177
|
|
|
178
|
+
# Alias for callers that want to make the exact-length contract explicit
|
|
179
|
+
# at the call site.
|
|
180
|
+
alias read_exact read
|
|
181
|
+
|
|
174
182
|
def write(bytes)
|
|
175
183
|
@sock.write(bytes)
|
|
176
184
|
rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, IOError => e
|
|
@@ -193,8 +201,8 @@ module Dalli
|
|
|
193
201
|
|
|
194
202
|
# Non-blocking read. Here to support the operation
|
|
195
203
|
# of the get_multi operation
|
|
196
|
-
def
|
|
197
|
-
@sock.read_available
|
|
204
|
+
def read_available(...)
|
|
205
|
+
@sock.read_available(...)
|
|
198
206
|
end
|
|
199
207
|
|
|
200
208
|
def max_allowed_failures
|
|
@@ -252,7 +260,7 @@ module Dalli
|
|
|
252
260
|
end
|
|
253
261
|
|
|
254
262
|
def fork_detected?
|
|
255
|
-
@pid && @pid !=
|
|
263
|
+
@pid && @pid != Process.pid
|
|
256
264
|
end
|
|
257
265
|
|
|
258
266
|
def log_down_detected
|
|
@@ -273,6 +281,22 @@ module Dalli
|
|
|
273
281
|
time = Time.now - @down_at
|
|
274
282
|
Dalli.logger.warn { format('%<name>s is back (downtime was %<time>.3f seconds)', name: name, time: time) }
|
|
275
283
|
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# Reads exactly `count` bytes. IO#read(count) on a blocking socket blocks
|
|
288
|
+
# until it has `count` bytes, accumulating across TCP chunks internally,
|
|
289
|
+
# and only hands back a shorter (or nil) buffer when the stream hits EOF.
|
|
290
|
+
# So a short read means the peer closed mid-response: raise EOFError and
|
|
291
|
+
# let read's existing `rescue EOFError` route it through
|
|
292
|
+
# error_on_request!, which closes the dirty socket for a retry on a fresh
|
|
293
|
+
# connection (and preserves the $ERROR_INFO context down! relies on).
|
|
294
|
+
def read_bytes(count)
|
|
295
|
+
buffer = @sock.read(count)
|
|
296
|
+
return buffer if buffer && buffer.bytesize == count
|
|
297
|
+
|
|
298
|
+
raise EOFError, "EOF reading #{count} bytes; received #{buffer ? buffer.bytesize : 0}"
|
|
299
|
+
end
|
|
276
300
|
end
|
|
277
301
|
end
|
|
278
302
|
end
|
|
@@ -9,19 +9,35 @@ module Dalli
|
|
|
9
9
|
# allowed.
|
|
10
10
|
# memcached supports the use of base64 hashes for keys containing
|
|
11
11
|
# whitespace or non-ASCII characters, provided the 'b' flag is included in the request.
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
module KeyRegularizer
|
|
13
|
+
module_function
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
# protocol.txt requires that a key "must not include control
|
|
16
|
+
# characters or whitespace" -- \p{Cntrl} is C0 (0x00-0x1F) plus DEL
|
|
17
|
+
# (0x7F). \s alone misses NUL and the rest of that range: a key
|
|
18
|
+
# containing one of those bytes but no whitespace is ASCII-only, so
|
|
19
|
+
# it would otherwise be written to the wire unencoded. Not a
|
|
20
|
+
# protocol-injection risk (the text protocol splits on CRLF, not
|
|
21
|
+
# other control bytes), but a downstream consumer that treats the key
|
|
22
|
+
# specially at one of those bytes (a C string terminating at NUL, a
|
|
23
|
+
# terminal or log line interpreting an escape byte) could silently
|
|
24
|
+
# act on a different key than Dalli believes it sent.
|
|
25
|
+
#
|
|
26
|
+
# Written as \p{Cntrl} rather than the POSIX [:cntrl:] bracket class:
|
|
27
|
+
# \s and [:cntrl:] overlap (tab, newline, CR are in both), and Ruby
|
|
28
|
+
# warns "character class has duplicated range" when they're combined
|
|
29
|
+
# in one -- fatal here, since this suite's -w run treats warnings as
|
|
30
|
+
# errors (see test_strict_warnings.rb). \p{Cntrl} matches the same
|
|
31
|
+
# bytes without the overlap warning.
|
|
32
|
+
def required?(key)
|
|
33
|
+
!key.ascii_only? || /[\p{Cntrl}\s]/.match?(key)
|
|
20
34
|
end
|
|
21
35
|
|
|
22
|
-
def
|
|
23
|
-
|
|
36
|
+
def encode(key)
|
|
37
|
+
[key].pack('m0')
|
|
38
|
+
end
|
|
24
39
|
|
|
40
|
+
def decode(encoded_key)
|
|
25
41
|
strict_base64_decoded = encoded_key.unpack1('m0')
|
|
26
42
|
strict_base64_decoded.force_encoding(Encoding::UTF_8)
|
|
27
43
|
end
|