redis 3.3.5 → 4.1.4

Sign up to get free protection for your applications and to get access to all the features.
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.