redis 4.0.1 → 4.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.
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"