redis 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +46 -1
  4. data/lib/redis/client.rb +29 -12
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +34 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +104 -0
  9. data/lib/redis/cluster/node_key.rb +35 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +77 -0
  12. data/lib/redis/cluster/slot.rb +69 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +286 -0
  15. data/lib/redis/connection/ruby.rb +5 -2
  16. data/lib/redis/distributed.rb +13 -6
  17. data/lib/redis/errors.rb +46 -0
  18. data/lib/redis/pipeline.rb +9 -1
  19. data/lib/redis/version.rb +1 -1
  20. data/lib/redis.rb +692 -25
  21. metadata +27 -184
  22. data/.gitignore +0 -16
  23. data/.travis/Gemfile +0 -13
  24. data/.travis.yml +0 -73
  25. data/.yardopts +0 -3
  26. data/Gemfile +0 -3
  27. data/benchmarking/logging.rb +0 -71
  28. data/benchmarking/pipeline.rb +0 -51
  29. data/benchmarking/speed.rb +0 -21
  30. data/benchmarking/suite.rb +0 -24
  31. data/benchmarking/worker.rb +0 -71
  32. data/bors.toml +0 -14
  33. data/examples/basic.rb +0 -15
  34. data/examples/consistency.rb +0 -114
  35. data/examples/dist_redis.rb +0 -43
  36. data/examples/incr-decr.rb +0 -17
  37. data/examples/list.rb +0 -26
  38. data/examples/pubsub.rb +0 -37
  39. data/examples/sentinel/sentinel.conf +0 -9
  40. data/examples/sentinel/start +0 -49
  41. data/examples/sentinel.rb +0 -41
  42. data/examples/sets.rb +0 -36
  43. data/examples/unicorn/config.ru +0 -3
  44. data/examples/unicorn/unicorn.rb +0 -20
  45. data/makefile +0 -42
  46. data/redis.gemspec +0 -42
  47. data/test/bitpos_test.rb +0 -63
  48. data/test/blocking_commands_test.rb +0 -40
  49. data/test/client_test.rb +0 -59
  50. data/test/command_map_test.rb +0 -28
  51. data/test/commands_on_hashes_test.rb +0 -19
  52. data/test/commands_on_hyper_log_log_test.rb +0 -19
  53. data/test/commands_on_lists_test.rb +0 -18
  54. data/test/commands_on_sets_test.rb +0 -75
  55. data/test/commands_on_sorted_sets_test.rb +0 -150
  56. data/test/commands_on_strings_test.rb +0 -99
  57. data/test/commands_on_value_types_test.rb +0 -171
  58. data/test/connection_handling_test.rb +0 -275
  59. data/test/connection_test.rb +0 -57
  60. data/test/db/.gitkeep +0 -0
  61. data/test/distributed_blocking_commands_test.rb +0 -44
  62. data/test/distributed_commands_on_hashes_test.rb +0 -8
  63. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -31
  64. data/test/distributed_commands_on_lists_test.rb +0 -20
  65. data/test/distributed_commands_on_sets_test.rb +0 -106
  66. data/test/distributed_commands_on_sorted_sets_test.rb +0 -16
  67. data/test/distributed_commands_on_strings_test.rb +0 -69
  68. data/test/distributed_commands_on_value_types_test.rb +0 -93
  69. data/test/distributed_commands_requiring_clustering_test.rb +0 -162
  70. data/test/distributed_connection_handling_test.rb +0 -21
  71. data/test/distributed_internals_test.rb +0 -68
  72. data/test/distributed_key_tags_test.rb +0 -50
  73. data/test/distributed_persistence_control_commands_test.rb +0 -24
  74. data/test/distributed_publish_subscribe_test.rb +0 -90
  75. data/test/distributed_remote_server_control_commands_test.rb +0 -64
  76. data/test/distributed_scripting_test.rb +0 -100
  77. data/test/distributed_sorting_test.rb +0 -18
  78. data/test/distributed_test.rb +0 -56
  79. data/test/distributed_transactions_test.rb +0 -30
  80. data/test/encoding_test.rb +0 -14
  81. data/test/error_replies_test.rb +0 -57
  82. data/test/fork_safety_test.rb +0 -60
  83. data/test/helper.rb +0 -201
  84. data/test/helper_test.rb +0 -22
  85. data/test/internals_test.rb +0 -389
  86. data/test/lint/blocking_commands.rb +0 -150
  87. data/test/lint/hashes.rb +0 -162
  88. data/test/lint/hyper_log_log.rb +0 -60
  89. data/test/lint/lists.rb +0 -143
  90. data/test/lint/sets.rb +0 -140
  91. data/test/lint/sorted_sets.rb +0 -316
  92. data/test/lint/strings.rb +0 -246
  93. data/test/lint/value_types.rb +0 -130
  94. data/test/persistence_control_commands_test.rb +0 -24
  95. data/test/pipelining_commands_test.rb +0 -238
  96. data/test/publish_subscribe_test.rb +0 -280
  97. data/test/remote_server_control_commands_test.rb +0 -175
  98. data/test/scanning_test.rb +0 -407
  99. data/test/scripting_test.rb +0 -76
  100. data/test/sentinel_command_test.rb +0 -78
  101. data/test/sentinel_test.rb +0 -253
  102. data/test/sorting_test.rb +0 -57
  103. data/test/ssl_test.rb +0 -69
  104. data/test/support/connection/hiredis.rb +0 -1
  105. data/test/support/connection/ruby.rb +0 -1
  106. data/test/support/connection/synchrony.rb +0 -17
  107. data/test/support/redis_mock.rb +0 -130
  108. data/test/support/ssl/gen_certs.sh +0 -31
  109. data/test/support/ssl/trusted-ca.crt +0 -25
  110. data/test/support/ssl/trusted-ca.key +0 -27
  111. data/test/support/ssl/trusted-cert.crt +0 -81
  112. data/test/support/ssl/trusted-cert.key +0 -28
  113. data/test/support/ssl/untrusted-ca.crt +0 -26
  114. data/test/support/ssl/untrusted-ca.key +0 -27
  115. data/test/support/ssl/untrusted-cert.crt +0 -82
  116. data/test/support/ssl/untrusted-cert.key +0 -28
  117. data/test/support/wire/synchrony.rb +0 -24
  118. data/test/support/wire/thread.rb +0 -5
  119. data/test/synchrony_driver.rb +0 -85
  120. data/test/test.conf.erb +0 -9
  121. data/test/thread_safety_test.rb +0 -60
  122. data/test/transactions_test.rb +0 -262
  123. data/test/unknown_commands_test.rb +0 -12
  124. data/test/url_param_test.rb +0 -136
data/lib/redis.rb CHANGED
@@ -31,11 +31,16 @@ class Redis
31
31
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
32
32
  # @option options [Array] :sentinels List of sentinels to contact
33
33
  # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
34
+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
35
+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
36
+ # @option options [Class] :connector Class of custom connector
34
37
  #
35
38
  # @return [Redis] a new client instance
36
39
  def initialize(options = {})
37
40
  @options = options.dup
38
- @original_client = @client = Client.new(options)
41
+ @cluster_mode = options.key?(:cluster)
42
+ client = @cluster_mode ? Cluster : Client
43
+ @original_client = @client = client.new(options)
39
44
  @queue = Hash.new { |h, k| h[k] = [] }
40
45
 
41
46
  super() # Monitor#initialize
@@ -273,9 +278,7 @@ class Redis
273
278
  synchronize do |client|
274
279
  client.call([:info, cmd].compact) do |reply|
275
280
  if reply.kind_of?(String)
276
- reply = Hash[reply.split("\r\n").map do |line|
277
- line.split(":", 2) unless line =~ /^(#|$)/
278
- end.compact]
281
+ reply = HashifyInfo.call(reply)
279
282
 
280
283
  if cmd && cmd.to_s == "commandstats"
281
284
  # Extract nested hashes for INFO COMMANDSTATS
@@ -496,22 +499,27 @@ class Redis
496
499
 
497
500
  # Transfer a key from the connected instance to another instance.
498
501
  #
499
- # @param [String] key
502
+ # @param [String, Array<String>] key
500
503
  # @param [Hash] options
501
504
  # - `:host => String`: host of instance to migrate to
502
505
  # - `:port => Integer`: port of instance to migrate to
503
506
  # - `:db => Integer`: database to migrate to (default: same as source)
504
507
  # - `:timeout => Integer`: timeout (default: same as connection timeout)
508
+ # - `:copy => Boolean`: Do not remove the key from the local instance.
509
+ # - `:replace => Boolean`: Replace existing key on the remote instance.
505
510
  # @return [String] `"OK"`
506
511
  def migrate(key, options)
507
- host = options[:host] || raise(RuntimeError, ":host not specified")
508
- port = options[:port] || raise(RuntimeError, ":port not specified")
509
- db = (options[:db] || @client.db).to_i
510
- timeout = (options[:timeout] || @client.timeout).to_i
512
+ args = [:migrate]
513
+ args << (options[:host] || raise(':host not specified'))
514
+ args << (options[:port] || raise(':port not specified'))
515
+ args << (key.is_a?(String) ? key : '')
516
+ args << (options[:db] || @client.db).to_i
517
+ args << (options[:timeout] || @client.timeout).to_i
518
+ args << 'COPY' if options[:copy]
519
+ args << 'REPLACE' if options[:replace]
520
+ args += ['KEYS', *key] if key.is_a?(Array)
511
521
 
512
- synchronize do |client|
513
- client.call([:migrate, host, port, key, db, timeout])
514
- end
522
+ synchronize { |client| client.call(args) }
515
523
  end
516
524
 
517
525
  # Delete one or more keys.
@@ -524,6 +532,16 @@ class Redis
524
532
  end
525
533
  end
526
534
 
535
+ # Unlink one or more keys.
536
+ #
537
+ # @param [String, Array<String>] keys
538
+ # @return [Fixnum] number of keys that were unlinked
539
+ def unlink(*keys)
540
+ synchronize do |client|
541
+ client.call([:unlink] + keys)
542
+ end
543
+ end
544
+
527
545
  # Determine if a key exists.
528
546
  #
529
547
  # @param [String] key
@@ -1138,15 +1156,14 @@ class Redis
1138
1156
  end
1139
1157
  end
1140
1158
 
1141
- def _bpop(cmd, args)
1159
+ def _bpop(cmd, args, &blk)
1142
1160
  options = {}
1143
1161
 
1144
- case args.last
1145
- when Hash
1162
+ if args.last.is_a?(Hash)
1146
1163
  options = args.pop
1147
- when Integer
1164
+ elsif args.last.respond_to?(:to_int)
1148
1165
  # Issue deprecation notice in obnoxious mode...
1149
- options[:timeout] = args.pop
1166
+ options[:timeout] = args.pop.to_int
1150
1167
  end
1151
1168
 
1152
1169
  if args.size > 1
@@ -1159,7 +1176,7 @@ class Redis
1159
1176
  synchronize do |client|
1160
1177
  command = [cmd, keys, timeout]
1161
1178
  timeout += client.timeout if timeout > 0
1162
- client.call_with_timeout(command, timeout)
1179
+ client.call_with_timeout(command, timeout, &blk)
1163
1180
  end
1164
1181
  end
1165
1182
 
@@ -1610,6 +1627,90 @@ class Redis
1610
1627
  end
1611
1628
  end
1612
1629
 
1630
+ # Removes and returns up to count members with the highest scores in the sorted set stored at key.
1631
+ #
1632
+ # @example Popping a member
1633
+ # redis.zpopmax('zset')
1634
+ # #=> ['b', 2.0]
1635
+ # @example With count option
1636
+ # redis.zpopmax('zset', 2)
1637
+ # #=> [['b', 2.0], ['a', 1.0]]
1638
+ #
1639
+ # @params key [String] a key of the sorted set
1640
+ # @params count [Integer] a number of members
1641
+ #
1642
+ # @return [Array<String, Float>] element and score pair if count is not specified
1643
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1644
+ def zpopmax(key, count = nil)
1645
+ synchronize do |client|
1646
+ members = client.call([:zpopmax, key, count].compact, &FloatifyPairs)
1647
+ count.to_i > 1 ? members : members.first
1648
+ end
1649
+ end
1650
+
1651
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at key.
1652
+ #
1653
+ # @example Popping a member
1654
+ # redis.zpopmin('zset')
1655
+ # #=> ['a', 1.0]
1656
+ # @example With count option
1657
+ # redis.zpopmin('zset', 2)
1658
+ # #=> [['a', 1.0], ['b', 2.0]]
1659
+ #
1660
+ # @params key [String] a key of the sorted set
1661
+ # @params count [Integer] a number of members
1662
+ #
1663
+ # @return [Array<String, Float>] element and score pair if count is not specified
1664
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1665
+ def zpopmin(key, count = nil)
1666
+ synchronize do |client|
1667
+ members = client.call([:zpopmin, key, count].compact, &FloatifyPairs)
1668
+ count.to_i > 1 ? members : members.first
1669
+ end
1670
+ end
1671
+
1672
+ # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
1673
+ # or block until one is available.
1674
+ #
1675
+ # @example Popping a member from a sorted set
1676
+ # redis.bzpopmax('zset', 1)
1677
+ # #=> ['zset', 'b', 2.0]
1678
+ # @example Popping a member from multiple sorted sets
1679
+ # redis.bzpopmax('zset1', 'zset2', 1)
1680
+ # #=> ['zset1', 'b', 2.0]
1681
+ #
1682
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1683
+ # @params timeout [Integer] the maximum number of seconds to block
1684
+ #
1685
+ # @return [Array<String, String, Float>] a touple of key, member and score
1686
+ # @return [nil] when no element could be popped and the timeout expired
1687
+ def bzpopmax(*args)
1688
+ _bpop(:bzpopmax, args) do |reply|
1689
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1690
+ end
1691
+ end
1692
+
1693
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at keys,
1694
+ # or block until one is available.
1695
+ #
1696
+ # @example Popping a member from a sorted set
1697
+ # redis.bzpopmin('zset', 1)
1698
+ # #=> ['zset', 'a', 1.0]
1699
+ # @example Popping a member from multiple sorted sets
1700
+ # redis.bzpopmin('zset1', 'zset2', 1)
1701
+ # #=> ['zset1', 'a', 1.0]
1702
+ #
1703
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1704
+ # @params timeout [Integer] the maximum number of seconds to block
1705
+ #
1706
+ # @return [Array<String, String, Float>] a touple of key, member and score
1707
+ # @return [nil] when no element could be popped and the timeout expired
1708
+ def bzpopmin(*args)
1709
+ _bpop(:bzpopmin, args) do |reply|
1710
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1711
+ end
1712
+ end
1713
+
1613
1714
  # Get the score associated with the given member in a sorted set.
1614
1715
  #
1615
1716
  # @example Get the score for member "a"
@@ -2102,9 +2203,9 @@ class Redis
2102
2203
  # @param [String] key
2103
2204
  # @param [String, Array<String>] field
2104
2205
  # @return [Fixnum] the number of fields that were removed from the hash
2105
- def hdel(key, field)
2206
+ def hdel(key, *fields)
2106
2207
  synchronize do |client|
2107
- client.call([:hdel, key, field])
2208
+ client.call([:hdel, key, *fields])
2108
2209
  end
2109
2210
  end
2110
2211
 
@@ -2710,6 +2811,425 @@ class Redis
2710
2811
  end
2711
2812
  end
2712
2813
 
2814
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
2815
+ #
2816
+ # @param [String] key
2817
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
2818
+ # @return [Intger] number of elements added to the sorted set
2819
+ def geoadd(key, *member)
2820
+ synchronize do |client|
2821
+ client.call([:geoadd, key, member])
2822
+ end
2823
+ end
2824
+
2825
+ # Returns geohash string representing position for specified members of the specified key.
2826
+ #
2827
+ # @param [String] key
2828
+ # @param [String, Array<String>] member one member or array of members
2829
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
2830
+ def geohash(key, member)
2831
+ synchronize do |client|
2832
+ client.call([:geohash, key, member])
2833
+ end
2834
+ end
2835
+
2836
+
2837
+ # Query a sorted set representing a geospatial index to fetch members matching a
2838
+ # given maximum distance from a point
2839
+ #
2840
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
2841
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest to the nearest relative to the center
2842
+ # @param [Integer] count limit the results to the first N matching items
2843
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2844
+ # @return [Array<String>] may be changed with `options`
2845
+
2846
+ def georadius(*args, **geoptions)
2847
+ geoarguments = _geoarguments(*args, **geoptions)
2848
+
2849
+ synchronize do |client|
2850
+ client.call([:georadius, *geoarguments])
2851
+ end
2852
+ end
2853
+
2854
+ # Query a sorted set representing a geospatial index to fetch members matching a
2855
+ # given maximum distance from an already existing member
2856
+ #
2857
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
2858
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest to the nearest relative to the center
2859
+ # @param [Integer] count limit the results to the first N matching items
2860
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2861
+ # @return [Array<String>] may be changed with `options`
2862
+
2863
+ def georadiusbymember(*args, **geoptions)
2864
+ geoarguments = _geoarguments(*args, **geoptions)
2865
+
2866
+ synchronize do |client|
2867
+ client.call([:georadiusbymember, *geoarguments])
2868
+ end
2869
+ end
2870
+
2871
+ # Returns longitude and latitude of members of a geospatial index
2872
+ #
2873
+ # @param [String] key
2874
+ # @param [String, Array<String>] member one member or array of members
2875
+ # @return [Array<Array<String>, nil>] returns array of elements, where each element is either array of longitude and latitude or nil
2876
+ def geopos(key, member)
2877
+ synchronize do |client|
2878
+ client.call([:geopos, key, member])
2879
+ end
2880
+ end
2881
+
2882
+ # Returns the distance between two members of a geospatial index
2883
+ #
2884
+ # @param [String ]key
2885
+ # @param [Array<String>] members
2886
+ # @param ['m', 'km', 'mi', 'ft'] unit
2887
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
2888
+ def geodist(key, member1, member2, unit = 'm')
2889
+ synchronize do |client|
2890
+ client.call([:geodist, key, member1, member2, unit])
2891
+ end
2892
+ end
2893
+
2894
+ # Returns the stream information each subcommand.
2895
+ #
2896
+ # @example stream
2897
+ # redis.xinfo(:stream, 'mystream')
2898
+ # @example groups
2899
+ # redis.xinfo(:groups, 'mystream')
2900
+ # @example consumers
2901
+ # redis.xinfo(:consumers, 'mystream', 'mygroup')
2902
+ #
2903
+ # @param subcommand [String] e.g. `stream` `groups` `consumers`
2904
+ # @param key [String] the stream key
2905
+ # @param group [String] the consumer group name, required if subcommand is `consumers`
2906
+ #
2907
+ # @return [Hash] information of the stream if subcommand is `stream`
2908
+ # @return [Array<Hash>] information of the consumer groups if subcommand is `groups`
2909
+ # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
2910
+ def xinfo(subcommand, key, group = nil)
2911
+ args = [:xinfo, subcommand, key, group].compact
2912
+ synchronize do |client|
2913
+ client.call(args) do |reply|
2914
+ case subcommand.to_s.downcase
2915
+ when 'stream' then Hashify.call(reply)
2916
+ when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
2917
+ else reply
2918
+ end
2919
+ end
2920
+ end
2921
+ end
2922
+
2923
+ # Add new entry to the stream.
2924
+ #
2925
+ # @example Without options
2926
+ # redis.xadd('mystream', f1: 'v1', f2: 'v2')
2927
+ # @example With options
2928
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
2929
+ #
2930
+ # @param key [String] the stream key
2931
+ # @param entry [Hash] one or multiple field-value pairs
2932
+ # @param opts [Hash] several options for `XADD` command
2933
+ #
2934
+ # @option opts [String] :id the entry id, default value is `*`, it means auto generation
2935
+ # @option opts [Integer] :maxlen max length of entries
2936
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
2937
+ #
2938
+ # @return [String] the entry id
2939
+ def xadd(key, entry, opts = {})
2940
+ args = [:xadd, key]
2941
+ args.concat(['MAXLEN', (opts[:approximate] ? '~' : nil), opts[:maxlen]].compact) if opts[:maxlen]
2942
+ args << (opts[:id] || '*')
2943
+ args.concat(entry.to_a.flatten)
2944
+ synchronize { |client| client.call(args) }
2945
+ end
2946
+
2947
+ # Trims older entries of the stream if needed.
2948
+ #
2949
+ # @example Without options
2950
+ # redis.xtrim('mystream', 1000)
2951
+ # @example With options
2952
+ # redis.xtrim('mystream', 1000, approximate: true)
2953
+ #
2954
+ # @param key [String] the stream key
2955
+ # @param mexlen [Integer] max length of entries
2956
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
2957
+ #
2958
+ # @return [Integer] the number of entries actually deleted
2959
+ def xtrim(key, maxlen, approximate: false)
2960
+ args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
2961
+ synchronize { |client| client.call(args) }
2962
+ end
2963
+
2964
+ # Delete entries by entry ids.
2965
+ #
2966
+ # @example With splatted entry ids
2967
+ # redis.xdel('mystream', '0-1', '0-2')
2968
+ # @example With arrayed entry ids
2969
+ # redis.xdel('mystream', ['0-1', '0-2'])
2970
+ #
2971
+ # @param key [String] the stream key
2972
+ # @param ids [Array<String>] one or multiple entry ids
2973
+ #
2974
+ # @return [Integer] the number of entries actually deleted
2975
+ def xdel(key, *ids)
2976
+ args = [:xdel, key].concat(ids.flatten)
2977
+ synchronize { |client| client.call(args) }
2978
+ end
2979
+
2980
+ # Fetches entries of the stream.
2981
+ #
2982
+ # @example Without options
2983
+ # redis.xrange('mystream')
2984
+ # @example With first entry id option
2985
+ # redis.xrange('mystream', first: '0-1')
2986
+ # @example With first and last entry id options
2987
+ # redis.xrange('mystream', first: '0-1', last: '0-3')
2988
+ # @example With count options
2989
+ # redis.xrange('mystream', count: 10)
2990
+ #
2991
+ # @param key [String] the stream key
2992
+ # @param start [String] first entry id of range, default value is `+`
2993
+ # @param end [String] last entry id of range, default value is `-`
2994
+ # @param count [Integer] the number of entries as limit
2995
+ #
2996
+ # @return [Hash{String => Hash}] the entries
2997
+
2998
+ # Fetches entries of the stream in ascending order.
2999
+ #
3000
+ # @example Without options
3001
+ # redis.xrange('mystream')
3002
+ # @example With a specific start
3003
+ # redis.xrange('mystream', '0-1')
3004
+ # @example With a specific start and end
3005
+ # redis.xrange('mystream', '0-1', '0-3')
3006
+ # @example With count options
3007
+ # redis.xrange('mystream', count: 10)
3008
+ #
3009
+ # @param key [String] the stream key
3010
+ # @param start [String] first entry id of range, default value is `-`
3011
+ # @param end [String] last entry id of range, default value is `+`
3012
+ # @param count [Integer] the number of entries as limit
3013
+ #
3014
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3015
+ def xrange(key, start = '-', _end = '+', count: nil)
3016
+ args = [:xrange, key, start, _end]
3017
+ args.concat(['COUNT', count]) if count
3018
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3019
+ end
3020
+
3021
+ # Fetches entries of the stream in descending order.
3022
+ #
3023
+ # @example Without options
3024
+ # redis.xrevrange('mystream')
3025
+ # @example With a specific end
3026
+ # redis.xrevrange('mystream', '0-3')
3027
+ # @example With a specific end and start
3028
+ # redis.xrevrange('mystream', '0-3', '0-1')
3029
+ # @example With count options
3030
+ # redis.xrevrange('mystream', count: 10)
3031
+ #
3032
+ # @param key [String] the stream key
3033
+ # @param end [String] first entry id of range, default value is `+`
3034
+ # @param start [String] last entry id of range, default value is `-`
3035
+ # @params count [Integer] the number of entries as limit
3036
+ #
3037
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3038
+ def xrevrange(key, _end = '+', start = '-', count: nil)
3039
+ args = [:xrevrange, key, _end, start]
3040
+ args.concat(['COUNT', count]) if count
3041
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3042
+ end
3043
+
3044
+ # Returns the number of entries inside a stream.
3045
+ #
3046
+ # @example With key
3047
+ # redis.xlen('mystream')
3048
+ #
3049
+ # @param key [String] the stream key
3050
+ #
3051
+ # @return [Integer] the number of entries
3052
+ def xlen(key)
3053
+ synchronize { |client| client.call([:xlen, key]) }
3054
+ end
3055
+
3056
+ # Fetches entries from one or multiple streams. Optionally blocking.
3057
+ #
3058
+ # @example With a key
3059
+ # redis.xread('mystream', '0-0')
3060
+ # @example With multiple keys
3061
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
3062
+ # @example With count option
3063
+ # redis.xread('mystream', '0-0', count: 2)
3064
+ # @example With block option
3065
+ # redis.xread('mystream', '$', block: 1000)
3066
+ #
3067
+ # @param keys [Array<String>] one or multiple stream keys
3068
+ # @param ids [Array<String>] one or multiple entry ids
3069
+ # @param count [Integer] the number of entries as limit per stream
3070
+ # @param block [Integer] the number of milliseconds as blocking timeout
3071
+ #
3072
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3073
+ def xread(keys, ids, count: nil, block: nil)
3074
+ args = [:xread]
3075
+ args << 'COUNT' << count if count
3076
+ args << 'BLOCK' << block.to_i if block
3077
+ _xread(args, keys, ids, block)
3078
+ end
3079
+
3080
+ # Manages the consumer group of the stream.
3081
+ #
3082
+ # @example With `create` subcommand
3083
+ # redis.xgroup(:create, 'mystream', 'mygroup', '$')
3084
+ # @example With `setid` subcommand
3085
+ # redis.xgroup(:setid, 'mystream', 'mygroup', '$')
3086
+ # @example With `destroy` subcommand
3087
+ # redis.xgroup(:destroy, 'mystream', 'mygroup')
3088
+ # @example With `delconsumer` subcommand
3089
+ # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1')
3090
+ #
3091
+ # @param subcommand [String] `create` `setid` `destroy` `delconsumer`
3092
+ # @param key [String] the stream key
3093
+ # @param group [String] the consumer group name
3094
+ # @param id_or_consumer [String]
3095
+ # * the entry id or `$`, required if subcommand is `create` or `setid`
3096
+ # * the consumer name, required if subcommand is `delconsumer`
3097
+ # @param mkstream [Boolean] whether to create an empty stream automatically or not
3098
+ #
3099
+ # @return [String] `OK` if subcommand is `create` or `setid`
3100
+ # @return [Integer] effected count if subcommand is `destroy` or `delconsumer`
3101
+ def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false)
3102
+ args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact
3103
+ synchronize { |client| client.call(args) }
3104
+ end
3105
+
3106
+ # Fetches a subset of the entries from one or multiple streams related with the consumer group.
3107
+ # Optionally blocking.
3108
+ #
3109
+ # @example With a key
3110
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>')
3111
+ # @example With multiple keys
3112
+ # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >])
3113
+ # @example With count option
3114
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2)
3115
+ # @example With block option
3116
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000)
3117
+ # @example With noack option
3118
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true)
3119
+ #
3120
+ # @param group [String] the consumer group name
3121
+ # @param consumer [String] the consumer name
3122
+ # @param keys [Array<String>] one or multiple stream keys
3123
+ # @param ids [Array<String>] one or multiple entry ids
3124
+ # @param opts [Hash] several options for `XREADGROUP` command
3125
+ #
3126
+ # @option opts [Integer] :count the number of entries as limit
3127
+ # @option opts [Integer] :block the number of milliseconds as blocking timeout
3128
+ # @option opts [Boolean] :noack whether message loss is acceptable or not
3129
+ #
3130
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3131
+ def xreadgroup(group, consumer, keys, ids, opts = {})
3132
+ args = [:xreadgroup, 'GROUP', group, consumer]
3133
+ args << 'COUNT' << opts[:count] if opts[:count]
3134
+ args << 'BLOCK' << opts[:block].to_i if opts[:block]
3135
+ args << 'NOACK' if opts[:noack]
3136
+ _xread(args, keys, ids, opts[:block])
3137
+ end
3138
+
3139
+ # Removes one or multiple entries from the pending entries list of a stream consumer group.
3140
+ #
3141
+ # @example With a entry id
3142
+ # redis.xack('mystream', 'mygroup', '1526569495631-0')
3143
+ # @example With splatted entry ids
3144
+ # redis.xack('mystream', 'mygroup', '0-1', '0-2')
3145
+ # @example With arrayed entry ids
3146
+ # redis.xack('mystream', 'mygroup', %w[0-1 0-2])
3147
+ #
3148
+ # @param key [String] the stream key
3149
+ # @param group [String] the consumer group name
3150
+ # @param ids [Array<String>] one or multiple entry ids
3151
+ #
3152
+ # @return [Integer] the number of entries successfully acknowledged
3153
+ def xack(key, group, *ids)
3154
+ args = [:xack, key, group].concat(ids.flatten)
3155
+ synchronize { |client| client.call(args) }
3156
+ end
3157
+
3158
+ # Changes the ownership of a pending entry
3159
+ #
3160
+ # @example With splatted entry ids
3161
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2')
3162
+ # @example With arrayed entry ids
3163
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2])
3164
+ # @example With idle option
3165
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000)
3166
+ # @example With time option
3167
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000)
3168
+ # @example With retrycount option
3169
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10)
3170
+ # @example With force option
3171
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true)
3172
+ # @example With justid option
3173
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true)
3174
+ #
3175
+ # @param key [String] the stream key
3176
+ # @param group [String] the consumer group name
3177
+ # @param consumer [String] the consumer name
3178
+ # @param min_idle_time [Integer] the number of milliseconds
3179
+ # @param ids [Array<String>] one or multiple entry ids
3180
+ # @param opts [Hash] several options for `XCLAIM` command
3181
+ #
3182
+ # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry
3183
+ # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time
3184
+ # @option opts [Integer] :retrycount the number of retry counter
3185
+ # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not
3186
+ # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not
3187
+ #
3188
+ # @return [Hash{String => Hash}] the entries successfully claimed
3189
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3190
+ def xclaim(key, group, consumer, min_idle_time, *ids, **opts)
3191
+ args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten)
3192
+ args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle]
3193
+ args.concat(['TIME', opts[:time].to_i]) if opts[:time]
3194
+ args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount]
3195
+ args << 'FORCE' if opts[:force]
3196
+ args << 'JUSTID' if opts[:justid]
3197
+ blk = opts[:justid] ? Noop : HashifyStreamEntries
3198
+ synchronize { |client| client.call(args, &blk) }
3199
+ end
3200
+
3201
+ # Fetches not acknowledging pending entries
3202
+ #
3203
+ # @example With key and group
3204
+ # redis.xpending('mystream', 'mygroup')
3205
+ # @example With range options
3206
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10)
3207
+ # @example With range and consumer options
3208
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
3209
+ #
3210
+ # @param key [String] the stream key
3211
+ # @param group [String] the consumer group name
3212
+ # @param start [String] start first entry id of range
3213
+ # @param end [String] end last entry id of range
3214
+ # @param count [Integer] count the number of entries as limit
3215
+ # @param consumer [String] the consumer name
3216
+ #
3217
+ # @return [Hash] the summary of pending entries
3218
+ # @return [Array<Hash>] the pending entries details if options were specified
3219
+ def xpending(key, group, *args)
3220
+ command_args = [:xpending, key, group]
3221
+ case args.size
3222
+ when 0, 3, 4
3223
+ command_args.concat(args)
3224
+ else
3225
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)"
3226
+ end
3227
+
3228
+ summary_needed = args.empty?
3229
+ blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails
3230
+ synchronize { |client| client.call(command_args, &blk) }
3231
+ end
3232
+
2713
3233
  # Interact with the sentinel command (masters, master, slaves, failover)
2714
3234
  #
2715
3235
  # @param [String] subcommand e.g. `masters`, `master`, `slaves`
@@ -2737,6 +3257,41 @@ class Redis
2737
3257
  end
2738
3258
  end
2739
3259
 
3260
+ # Sends `CLUSTER *` command to random node and returns its reply.
3261
+ #
3262
+ # @see https://redis.io/commands#cluster Reference of cluster command
3263
+ #
3264
+ # @param subcommand [String, Symbol] the subcommand of cluster command
3265
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
3266
+ #
3267
+ # @return [Object] depends on the subcommand
3268
+ def cluster(subcommand, *args)
3269
+ subcommand = subcommand.to_s.downcase
3270
+ block = case subcommand
3271
+ when 'slots' then HashifyClusterSlots
3272
+ when 'nodes' then HashifyClusterNodes
3273
+ when 'slaves' then HashifyClusterSlaves
3274
+ when 'info' then HashifyInfo
3275
+ else Noop
3276
+ end
3277
+
3278
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3279
+ block = Noop unless @cluster_mode
3280
+
3281
+ synchronize do |client|
3282
+ client.call([:cluster, subcommand] + args, &block)
3283
+ end
3284
+ end
3285
+
3286
+ # Sends `ASKING` command to random node and returns its reply.
3287
+ #
3288
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
3289
+ #
3290
+ # @return [String] `'OK'`
3291
+ def asking
3292
+ synchronize { |client| client.call(%i[asking]) }
3293
+ end
3294
+
2740
3295
  def id
2741
3296
  @original_client.id
2742
3297
  end
@@ -2750,6 +3305,8 @@ class Redis
2750
3305
  end
2751
3306
 
2752
3307
  def connection
3308
+ return @original_client.connection_info if @cluster_mode
3309
+
2753
3310
  {
2754
3311
  host: @original_client.host,
2755
3312
  port: @original_client.port,
@@ -2805,14 +3362,107 @@ private
2805
3362
  }
2806
3363
 
2807
3364
  FloatifyPairs =
2808
- lambda { |array|
2809
- if array
2810
- array.each_slice(2).map do |member, score|
2811
- [member, Floatify.call(score)]
2812
- end
3365
+ lambda { |result|
3366
+ result.each_slice(2).map do |member, score|
3367
+ [member, Floatify.call(score)]
3368
+ end
3369
+ }
3370
+
3371
+ HashifyInfo =
3372
+ lambda { |reply|
3373
+ Hash[reply.split("\r\n").map do |line|
3374
+ line.split(':', 2) unless line =~ /^(#|$)/
3375
+ end.compact]
3376
+ }
3377
+
3378
+ HashifyStreams =
3379
+ lambda { |reply|
3380
+ return {} if reply.nil?
3381
+ reply.map do |stream_key, entries|
3382
+ [stream_key, HashifyStreamEntries.call(entries)]
3383
+ end.to_h
3384
+ }
3385
+
3386
+ HashifyStreamEntries =
3387
+ lambda { |reply|
3388
+ reply.map do |entry_id, values|
3389
+ [entry_id, values.each_slice(2).to_h]
3390
+ end
3391
+ }
3392
+
3393
+ HashifyStreamPendings =
3394
+ lambda { |reply|
3395
+ {
3396
+ 'size' => reply[0],
3397
+ 'min_entry_id' => reply[1],
3398
+ 'max_entry_id' => reply[2],
3399
+ 'consumers' => reply[3].nil? ? {} : Hash[reply[3]]
3400
+ }
3401
+ }
3402
+
3403
+ HashifyStreamPendingDetails =
3404
+ lambda { |reply|
3405
+ reply.map do |arr|
3406
+ {
3407
+ 'entry_id' => arr[0],
3408
+ 'consumer' => arr[1],
3409
+ 'elapsed' => arr[2],
3410
+ 'count' => arr[3]
3411
+ }
2813
3412
  end
2814
3413
  }
2815
3414
 
3415
+ HashifyClusterNodeInfo =
3416
+ lambda { |str|
3417
+ arr = str.split(' ')
3418
+ {
3419
+ 'node_id' => arr[0],
3420
+ 'ip_port' => arr[1],
3421
+ 'flags' => arr[2].split(','),
3422
+ 'master_node_id' => arr[3],
3423
+ 'ping_sent' => arr[4],
3424
+ 'pong_recv' => arr[5],
3425
+ 'config_epoch' => arr[6],
3426
+ 'link_state' => arr[7],
3427
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3428
+ }
3429
+ }
3430
+
3431
+ HashifyClusterSlots =
3432
+ lambda { |reply|
3433
+ reply.map do |arr|
3434
+ first_slot, last_slot = arr[0..1]
3435
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3436
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3437
+ {
3438
+ 'start_slot' => first_slot,
3439
+ 'end_slot' => last_slot,
3440
+ 'master' => master,
3441
+ 'replicas' => replicas
3442
+ }
3443
+ end
3444
+ }
3445
+
3446
+ HashifyClusterNodes =
3447
+ lambda { |reply|
3448
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3449
+ }
3450
+
3451
+ HashifyClusterSlaves =
3452
+ lambda { |reply|
3453
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3454
+ }
3455
+
3456
+ Noop = ->(reply) { reply }
3457
+
3458
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
3459
+ args.push sort if sort
3460
+ args.push 'count', count if count
3461
+ args.push options if options
3462
+
3463
+ args.uniq
3464
+ end
3465
+
2816
3466
  def _subscription(method, timeout, channels, block)
2817
3467
  return @client.call([method] + channels) if subscribed?
2818
3468
 
@@ -2828,10 +3478,27 @@ private
2828
3478
  end
2829
3479
  end
2830
3480
 
3481
+ def _xread(args, keys, ids, blocking_timeout_msec)
3482
+ keys = keys.is_a?(Array) ? keys : [keys]
3483
+ ids = ids.is_a?(Array) ? ids : [ids]
3484
+ args << 'STREAMS'
3485
+ args.concat(keys)
3486
+ args.concat(ids)
3487
+
3488
+ synchronize do |client|
3489
+ if blocking_timeout_msec.nil?
3490
+ client.call(args, &HashifyStreams)
3491
+ else
3492
+ timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3493
+ client.call_with_timeout(args, timeout, &HashifyStreams)
3494
+ end
3495
+ end
3496
+ end
2831
3497
  end
2832
3498
 
2833
3499
  require_relative "redis/version"
2834
3500
  require_relative "redis/connection"
2835
3501
  require_relative "redis/client"
3502
+ require_relative "redis/cluster"
2836
3503
  require_relative "redis/pipeline"
2837
3504
  require_relative "redis/subscribe"