redis 3.3.5 → 4.1.4

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.
Files changed (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +84 -2
  3. data/README.md +131 -76
  4. data/lib/redis.rb +912 -200
  5. data/lib/redis/client.rb +71 -29
  6. data/lib/redis/cluster.rb +291 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +104 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +87 -0
  14. data/lib/redis/cluster/slot.rb +72 -0
  15. data/lib/redis/cluster/slot_loader.rb +50 -0
  16. data/lib/redis/connection.rb +3 -2
  17. data/lib/redis/connection/command_helper.rb +3 -8
  18. data/lib/redis/connection/hiredis.rb +3 -2
  19. data/lib/redis/connection/registry.rb +1 -0
  20. data/lib/redis/connection/ruby.rb +48 -32
  21. data/lib/redis/connection/synchrony.rb +13 -4
  22. data/lib/redis/distributed.rb +39 -15
  23. data/lib/redis/errors.rb +47 -0
  24. data/lib/redis/hash_ring.rb +21 -64
  25. data/lib/redis/pipeline.rb +54 -12
  26. data/lib/redis/subscribe.rb +1 -0
  27. data/lib/redis/version.rb +2 -1
  28. metadata +40 -198
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -89
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/client_test.rb +0 -59
  56. data/test/command_map_test.rb +0 -30
  57. data/test/commands_on_hashes_test.rb +0 -21
  58. data/test/commands_on_hyper_log_log_test.rb +0 -21
  59. data/test/commands_on_lists_test.rb +0 -20
  60. data/test/commands_on_sets_test.rb +0 -77
  61. data/test/commands_on_sorted_sets_test.rb +0 -137
  62. data/test/commands_on_strings_test.rb +0 -101
  63. data/test/commands_on_value_types_test.rb +0 -133
  64. data/test/connection_handling_test.rb +0 -277
  65. data/test/connection_test.rb +0 -57
  66. data/test/db/.gitkeep +0 -0
  67. data/test/distributed_blocking_commands_test.rb +0 -46
  68. data/test/distributed_commands_on_hashes_test.rb +0 -10
  69. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  70. data/test/distributed_commands_on_lists_test.rb +0 -22
  71. data/test/distributed_commands_on_sets_test.rb +0 -83
  72. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  73. data/test/distributed_commands_on_strings_test.rb +0 -59
  74. data/test/distributed_commands_on_value_types_test.rb +0 -95
  75. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  76. data/test/distributed_connection_handling_test.rb +0 -23
  77. data/test/distributed_internals_test.rb +0 -79
  78. data/test/distributed_key_tags_test.rb +0 -52
  79. data/test/distributed_persistence_control_commands_test.rb +0 -26
  80. data/test/distributed_publish_subscribe_test.rb +0 -92
  81. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  82. data/test/distributed_scripting_test.rb +0 -102
  83. data/test/distributed_sorting_test.rb +0 -20
  84. data/test/distributed_test.rb +0 -58
  85. data/test/distributed_transactions_test.rb +0 -32
  86. data/test/encoding_test.rb +0 -18
  87. data/test/error_replies_test.rb +0 -59
  88. data/test/fork_safety_test.rb +0 -65
  89. data/test/helper.rb +0 -232
  90. data/test/helper_test.rb +0 -24
  91. data/test/internals_test.rb +0 -417
  92. data/test/lint/blocking_commands.rb +0 -150
  93. data/test/lint/hashes.rb +0 -162
  94. data/test/lint/hyper_log_log.rb +0 -60
  95. data/test/lint/lists.rb +0 -143
  96. data/test/lint/sets.rb +0 -140
  97. data/test/lint/sorted_sets.rb +0 -316
  98. data/test/lint/strings.rb +0 -260
  99. data/test/lint/value_types.rb +0 -122
  100. data/test/persistence_control_commands_test.rb +0 -26
  101. data/test/pipelining_commands_test.rb +0 -242
  102. data/test/publish_subscribe_test.rb +0 -282
  103. data/test/remote_server_control_commands_test.rb +0 -118
  104. data/test/scanning_test.rb +0 -413
  105. data/test/scripting_test.rb +0 -78
  106. data/test/sentinel_command_test.rb +0 -80
  107. data/test/sentinel_test.rb +0 -255
  108. data/test/sorting_test.rb +0 -59
  109. data/test/ssl_test.rb +0 -73
  110. data/test/support/connection/hiredis.rb +0 -1
  111. data/test/support/connection/ruby.rb +0 -1
  112. data/test/support/connection/synchrony.rb +0 -17
  113. data/test/support/redis_mock.rb +0 -130
  114. data/test/support/ssl/gen_certs.sh +0 -31
  115. data/test/support/ssl/trusted-ca.crt +0 -25
  116. data/test/support/ssl/trusted-ca.key +0 -27
  117. data/test/support/ssl/trusted-cert.crt +0 -81
  118. data/test/support/ssl/trusted-cert.key +0 -28
  119. data/test/support/ssl/untrusted-ca.crt +0 -26
  120. data/test/support/ssl/untrusted-ca.key +0 -27
  121. data/test/support/ssl/untrusted-cert.crt +0 -82
  122. data/test/support/ssl/untrusted-cert.key +0 -28
  123. data/test/support/wire/synchrony.rb +0 -24
  124. data/test/support/wire/thread.rb +0 -5
  125. data/test/synchrony_driver.rb +0 -88
  126. data/test/test.conf.erb +0 -9
  127. data/test/thread_safety_test.rb +0 -62
  128. data/test/transactions_test.rb +0 -264
  129. data/test/unknown_commands_test.rb +0 -14
  130. data/test/url_param_test.rb +0 -138
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep slot and node key map for Redis Cluster Client
8
+ class Slot
9
+ ROLE_SLAVE = 'slave'
10
+
11
+ def initialize(available_slots, node_flags = {}, with_replica = false)
12
+ @with_replica = with_replica
13
+ @node_flags = node_flags
14
+ @map = build_slot_node_key_map(available_slots)
15
+ end
16
+
17
+ def exists?(slot)
18
+ @map.key?(slot)
19
+ end
20
+
21
+ def find_node_key_of_master(slot)
22
+ return nil unless exists?(slot)
23
+
24
+ @map[slot][:master]
25
+ end
26
+
27
+ def find_node_key_of_slave(slot)
28
+ return nil unless exists?(slot)
29
+ return find_node_key_of_master(slot) if replica_disabled?
30
+
31
+ @map[slot][:slaves].to_a.sample
32
+ end
33
+
34
+ def put(slot, node_key)
35
+ assign_node_key(@map, slot, node_key)
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ def replica_disabled?
42
+ !@with_replica
43
+ end
44
+
45
+ def master?(node_key)
46
+ !slave?(node_key)
47
+ end
48
+
49
+ def slave?(node_key)
50
+ @node_flags[node_key] == ROLE_SLAVE
51
+ end
52
+
53
+ # available_slots is mapping of node_key to list of slot ranges
54
+ def build_slot_node_key_map(available_slots)
55
+ available_slots.each_with_object({}) do |(node_key, slots_arr), acc|
56
+ slots_arr.each do |slots|
57
+ slots.each { |slot| assign_node_key(acc, slot, node_key) }
58
+ end
59
+ end
60
+ end
61
+
62
+ def assign_node_key(mappings, slot, node_key)
63
+ mappings[slot] ||= { master: nil, slaves: ::Set.new }
64
+ if master?(node_key)
65
+ mappings[slot][:master] = node_key
66
+ else
67
+ mappings[slot][:slaves].add(node_key)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'node_key'
5
+
6
+ class Redis
7
+ class Cluster
8
+ # Load and hashify slot info for Redis Cluster Client
9
+ module SlotLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ info = {}
14
+
15
+ nodes.each do |node|
16
+ info = fetch_slot_info(node)
17
+ info.empty? ? next : break
18
+ end
19
+
20
+ return info unless info.empty?
21
+
22
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
23
+ end
24
+
25
+ def fetch_slot_info(node)
26
+ hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
27
+ node.call(%i[cluster slots])
28
+ .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
+ .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
30
+
31
+ rescue CannotConnectError, ConnectionError, CommandError
32
+ {} # can retry on another node
33
+ end
34
+
35
+ def parse_slot_info(arr, default_ip:)
36
+ first_slot, last_slot = arr[0..1]
37
+ slot_range = (first_slot..last_slot).freeze
38
+ arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
39
+ end
40
+
41
+ def stringify_node_key(arr, default_ip)
42
+ ip, port = arr
43
+ ip = default_ip if ip.empty? # When cluster is down
44
+ NodeKey.build_from_host_port(ip, port)
45
+ end
46
+
47
+ private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key
48
+ end
49
+ end
50
+ end
@@ -1,4 +1,5 @@
1
- require "redis/connection/registry"
1
+ # frozen_string_literal: true
2
+ require_relative "connection/registry"
2
3
 
3
4
  # If a connection driver was required before this file, the array
4
5
  # Redis::Connection.drivers will contain one or more classes. The last driver
@@ -6,4 +7,4 @@ require "redis/connection/registry"
6
7
  # the plain Ruby driver as our default. Another driver can be required at a
7
8
  # later point in time, causing it to be the last element of the #drivers array
8
9
  # and therefore be chosen by default.
9
- require "redis/connection/ruby" if Redis::Connection.drivers.empty?
10
+ require_relative "connection/ruby" if Redis::Connection.drivers.empty?
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Redis
2
3
  module Connection
3
4
  module CommandHelper
@@ -30,14 +31,8 @@ class Redis
30
31
 
31
32
  protected
32
33
 
33
- if defined?(Encoding::default_external)
34
- def encode(string)
35
- string.force_encoding(Encoding::default_external)
36
- end
37
- else
38
- def encode(string)
39
- string
40
- end
34
+ def encode(string)
35
+ string.force_encoding(Encoding.default_external)
41
36
  end
42
37
  end
43
38
  end
@@ -1,5 +1,6 @@
1
- require "redis/connection/registry"
2
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+ require_relative "registry"
3
+ require_relative "../errors"
3
4
  require "hiredis/connection"
4
5
  require "timeout"
5
6
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Redis
2
3
  module Connection
3
4
 
@@ -1,6 +1,7 @@
1
- require "redis/connection/registry"
2
- require "redis/connection/command_helper"
3
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+ require_relative "registry"
3
+ require_relative "command_helper"
4
+ require_relative "../errors"
4
5
  require "socket"
5
6
  require "timeout"
6
7
 
@@ -10,36 +11,17 @@ rescue LoadError
10
11
  # Not all systems have OpenSSL support
11
12
  end
12
13
 
13
- if RUBY_VERSION < "1.9.3"
14
- class String
15
- # Ruby 1.8.7 does not have byteslice, but it handles encodings differently anyway.
16
- # We can simply slice the string, which is a byte array there.
17
- def byteslice(*args)
18
- slice(*args)
19
- end
20
- end
21
- end
22
-
23
14
  class Redis
24
15
  module Connection
25
16
  module SocketMixin
26
17
 
27
18
  CRLF = "\r\n".freeze
28
19
 
29
- # Exceptions raised during non-blocking I/O ops that require retrying the op
30
- if RUBY_VERSION >= "1.9.3"
31
- NBIO_READ_EXCEPTIONS = [IO::WaitReadable]
32
- NBIO_WRITE_EXCEPTIONS = [IO::WaitWritable]
33
- else
34
- NBIO_READ_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN]
35
- NBIO_WRITE_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN]
36
- end
37
-
38
20
  def initialize(*args)
39
21
  super(*args)
40
22
 
41
23
  @timeout = @write_timeout = nil
42
- @buffer = ""
24
+ @buffer = "".dup
43
25
  end
44
26
 
45
27
  def timeout=(timeout)
@@ -72,7 +54,7 @@ class Redis
72
54
  crlf = nil
73
55
 
74
56
  while (crlf = @buffer.index(CRLF)) == nil
75
- @buffer << _read_from_socket(1024)
57
+ @buffer << _read_from_socket(16384)
76
58
  end
77
59
 
78
60
  @buffer.slice!(0, crlf + CRLF.bytesize)
@@ -83,13 +65,13 @@ class Redis
83
65
  begin
84
66
  read_nonblock(nbytes)
85
67
 
86
- rescue *NBIO_READ_EXCEPTIONS
68
+ rescue IO::WaitReadable
87
69
  if IO.select([self], nil, nil, @timeout)
88
70
  retry
89
71
  else
90
72
  raise Redis::TimeoutError
91
73
  end
92
- rescue *NBIO_WRITE_EXCEPTIONS
74
+ rescue IO::WaitWritable
93
75
  if IO.select(nil, [self], nil, @timeout)
94
76
  retry
95
77
  else
@@ -105,13 +87,13 @@ class Redis
105
87
  begin
106
88
  write_nonblock(data)
107
89
 
108
- rescue *NBIO_WRITE_EXCEPTIONS
90
+ rescue IO::WaitWritable
109
91
  if IO.select(nil, [self], nil, @write_timeout)
110
92
  retry
111
93
  else
112
94
  raise Redis::TimeoutError
113
95
  end
114
- rescue *NBIO_READ_EXCEPTIONS
96
+ rescue IO::WaitReadable
115
97
  if IO.select([self], nil, nil, @write_timeout)
116
98
  retry
117
99
  else
@@ -285,8 +267,32 @@ class Redis
285
267
 
286
268
  ssl_sock = new(tcp_sock, ctx)
287
269
  ssl_sock.hostname = host
288
- ssl_sock.connect
289
- ssl_sock.post_connection_check(host)
270
+
271
+ begin
272
+ # Initiate the socket connection in the background. If it doesn't fail
273
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
274
+ # indicating the connection is in progress.
275
+ # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
276
+ # connections during the connect phase properly, because IO.select only partially works.
277
+ # Instead, you have to retry.
278
+ ssl_sock.connect_nonblock
279
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
280
+ if IO.select([ssl_sock], nil, nil, timeout)
281
+ retry
282
+ else
283
+ raise TimeoutError
284
+ end
285
+ rescue IO::WaitWritable
286
+ if IO.select(nil, [ssl_sock], nil, timeout)
287
+ retry
288
+ else
289
+ raise TimeoutError
290
+ end
291
+ end
292
+
293
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (ctx.respond_to?(:verify_hostname) && !ctx.verify_hostname)
294
+ ssl_sock.post_connection_check(host)
295
+ end
290
296
 
291
297
  ssl_sock
292
298
  end
@@ -307,16 +313,16 @@ class Redis
307
313
  raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
308
314
  sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
309
315
  elsif config[:scheme] == "rediss" || config[:ssl]
310
- raise ArgumentError, "This library does not support SSL on Ruby < 1.9" if RUBY_VERSION < "1.9.3"
311
316
  sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
312
317
  else
313
318
  sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
314
319
  end
315
320
 
316
321
  instance = new(sock)
317
- instance.timeout = config[:timeout]
322
+ instance.timeout = config[:read_timeout]
318
323
  instance.write_timeout = config[:write_timeout]
319
324
  instance.set_tcp_keepalive config[:tcp_keepalive]
325
+ instance.set_tcp_nodelay if sock.is_a? TCPSocket
320
326
  instance
321
327
  end
322
328
 
@@ -347,6 +353,16 @@ class Redis
347
353
  end
348
354
  end
349
355
 
356
+ # disables Nagle's Algorithm, prevents multiple round trips with MULTI
357
+ if [:IPPROTO_TCP, :TCP_NODELAY].all?{|c| Socket.const_defined? c}
358
+ def set_tcp_nodelay
359
+ @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
360
+ end
361
+ else
362
+ def set_tcp_nodelay
363
+ end
364
+ end
365
+
350
366
  def initialize(sock)
351
367
  @sock = sock
352
368
  end
@@ -1,6 +1,7 @@
1
- require "redis/connection/command_helper"
2
- require "redis/connection/registry"
3
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+ require_relative "command_helper"
3
+ require_relative "registry"
4
+ require_relative "../errors"
4
5
  require "em-synchrony"
5
6
  require "hiredis/reader"
6
7
 
@@ -72,7 +73,15 @@ class Redis
72
73
 
73
74
  def self.connect(config)
74
75
  if config[:scheme] == "unix"
75
- conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
76
+ begin
77
+ conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
78
+ rescue RuntimeError => e
79
+ if e.message == "no connection"
80
+ raise Errno::ECONNREFUSED
81
+ else
82
+ raise e
83
+ end
84
+ end
76
85
  elsif config[:scheme] == "rediss" || config[:ssl]
77
86
  raise NotImplementedError, "SSL not supported by synchrony driver"
78
87
  else
@@ -1,4 +1,5 @@
1
- require "redis/hash_ring"
1
+ # frozen_string_literal: true
2
+ require_relative "hash_ring"
2
3
 
3
4
  class Redis
4
5
  class Distributed
@@ -144,8 +145,8 @@ class Redis
144
145
  end
145
146
 
146
147
  # Create a key using the serialized value, previously obtained using DUMP.
147
- def restore(key, ttl, serialized_value)
148
- node_for(key).restore(key, ttl, serialized_value)
148
+ def restore(key, ttl, serialized_value, options = {})
149
+ node_for(key).restore(key, ttl, serialized_value, options)
149
150
  end
150
151
 
151
152
  # Transfer a key from the connected instance to another instance.
@@ -161,6 +162,14 @@ class Redis
161
162
  end
162
163
  end
163
164
 
165
+ # Unlink keys.
166
+ def unlink(*args)
167
+ keys_per_node = args.group_by { |key| node_for(key) }
168
+ keys_per_node.inject(0) do |sum, (node, keys)|
169
+ sum + node.unlink(*keys)
170
+ end
171
+ end
172
+
164
173
  # Determine if a key exists.
165
174
  def exists(key)
166
175
  node_for(key).exists(key)
@@ -277,13 +286,16 @@ class Redis
277
286
  node_for(key).get(key)
278
287
  end
279
288
 
280
- # Get the values of all the given keys.
289
+ # Get the values of all the given keys as an Array.
281
290
  def mget(*keys)
282
- raise CannotDistribute, :mget
291
+ mapped_mget(*keys).values_at(*keys)
283
292
  end
284
293
 
294
+ # Get the values of all the given keys as a Hash.
285
295
  def mapped_mget(*keys)
286
- raise CannotDistribute, :mapped_mget
296
+ keys.group_by { |k| node_for k }.inject({}) do |results, (node, subkeys)|
297
+ results.merge! node.mapped_mget(*subkeys)
298
+ end
287
299
  end
288
300
 
289
301
  # Overwrite part of a string at key starting at the specified offset.
@@ -390,14 +402,12 @@ class Redis
390
402
  end
391
403
 
392
404
  def _bpop(cmd, args)
393
- options = {}
394
-
395
- case args.last
396
- when Hash
405
+ timeout = if args.last.is_a?(Hash)
397
406
  options = args.pop
398
- when Integer
407
+ options[:timeout]
408
+ elsif args.last.respond_to?(:to_int)
399
409
  # Issue deprecation notice in obnoxious mode...
400
- options[:timeout] = args.pop
410
+ args.pop.to_int
401
411
  end
402
412
 
403
413
  if args.size > 1
@@ -407,7 +417,11 @@ class Redis
407
417
  keys = args.flatten
408
418
 
409
419
  ensure_same_node(cmd, keys) do |node|
410
- node.__send__(cmd, keys, options)
420
+ if timeout
421
+ node.__send__(cmd, keys, timeout: timeout)
422
+ else
423
+ node.__send__(cmd, keys)
424
+ end
411
425
  end
412
426
  end
413
427
 
@@ -509,6 +523,16 @@ class Redis
509
523
  node_for(key).smembers(key)
510
524
  end
511
525
 
526
+ # Scan a set
527
+ def sscan(key, cursor, options={})
528
+ node_for(key).sscan(key, cursor, options)
529
+ end
530
+
531
+ # Scan a set and return an enumerator
532
+ def sscan_each(key, options={}, &block)
533
+ node_for(key).sscan_each(key, options, &block)
534
+ end
535
+
512
536
  # Subtract multiple sets.
513
537
  def sdiff(*keys)
514
538
  ensure_same_node(:sdiff, keys) do |node|
@@ -679,8 +703,8 @@ class Redis
679
703
  end
680
704
 
681
705
  # Delete one or more hash fields.
682
- def hdel(key, field)
683
- node_for(key).hdel(key, field)
706
+ def hdel(key, *fields)
707
+ node_for(key).hdel(key, *fields)
684
708
  end
685
709
 
686
710
  # Determine if a hash field exists.