redis 4.1.0.beta1 → 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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -2
  3. data/README.md +45 -0
  4. data/lib/redis.rb +497 -20
  5. data/lib/redis/client.rb +14 -6
  6. data/lib/redis/cluster.rb +1 -0
  7. data/lib/redis/cluster/command_loader.rb +2 -0
  8. data/lib/redis/cluster/node_loader.rb +2 -0
  9. data/lib/redis/cluster/option.rb +1 -0
  10. data/lib/redis/cluster/slot_loader.rb +2 -0
  11. data/lib/redis/distributed.rb +3 -4
  12. data/lib/redis/version.rb +1 -1
  13. metadata +20 -243
  14. data/.gitignore +0 -19
  15. data/.travis.yml +0 -61
  16. data/.travis/Gemfile +0 -18
  17. data/.yardopts +0 -3
  18. data/Gemfile +0 -8
  19. data/benchmarking/logging.rb +0 -71
  20. data/benchmarking/pipeline.rb +0 -51
  21. data/benchmarking/speed.rb +0 -21
  22. data/benchmarking/suite.rb +0 -24
  23. data/benchmarking/worker.rb +0 -71
  24. data/bin/build +0 -71
  25. data/bors.toml +0 -14
  26. data/examples/basic.rb +0 -15
  27. data/examples/consistency.rb +0 -114
  28. data/examples/dist_redis.rb +0 -43
  29. data/examples/incr-decr.rb +0 -17
  30. data/examples/list.rb +0 -26
  31. data/examples/pubsub.rb +0 -37
  32. data/examples/sentinel.rb +0 -41
  33. data/examples/sentinel/start +0 -49
  34. data/examples/sets.rb +0 -36
  35. data/examples/unicorn/config.ru +0 -3
  36. data/examples/unicorn/unicorn.rb +0 -20
  37. data/makefile +0 -74
  38. data/redis.gemspec +0 -42
  39. data/test/bitpos_test.rb +0 -63
  40. data/test/blocking_commands_test.rb +0 -40
  41. data/test/client_test.rb +0 -76
  42. data/test/cluster_abnormal_state_test.rb +0 -38
  43. data/test/cluster_blocking_commands_test.rb +0 -15
  44. data/test/cluster_client_internals_test.rb +0 -77
  45. data/test/cluster_client_key_hash_tags_test.rb +0 -88
  46. data/test/cluster_client_options_test.rb +0 -147
  47. data/test/cluster_client_pipelining_test.rb +0 -59
  48. data/test/cluster_client_replicas_test.rb +0 -36
  49. data/test/cluster_client_slots_test.rb +0 -94
  50. data/test/cluster_client_transactions_test.rb +0 -71
  51. data/test/cluster_commands_on_cluster_test.rb +0 -165
  52. data/test/cluster_commands_on_connection_test.rb +0 -40
  53. data/test/cluster_commands_on_geo_test.rb +0 -74
  54. data/test/cluster_commands_on_hashes_test.rb +0 -11
  55. data/test/cluster_commands_on_hyper_log_log_test.rb +0 -17
  56. data/test/cluster_commands_on_keys_test.rb +0 -134
  57. data/test/cluster_commands_on_lists_test.rb +0 -15
  58. data/test/cluster_commands_on_pub_sub_test.rb +0 -101
  59. data/test/cluster_commands_on_scripting_test.rb +0 -56
  60. data/test/cluster_commands_on_server_test.rb +0 -221
  61. data/test/cluster_commands_on_sets_test.rb +0 -39
  62. data/test/cluster_commands_on_sorted_sets_test.rb +0 -35
  63. data/test/cluster_commands_on_streams_test.rb +0 -196
  64. data/test/cluster_commands_on_strings_test.rb +0 -15
  65. data/test/cluster_commands_on_transactions_test.rb +0 -41
  66. data/test/cluster_commands_on_value_types_test.rb +0 -14
  67. data/test/command_map_test.rb +0 -28
  68. data/test/commands_on_geo_test.rb +0 -116
  69. data/test/commands_on_hashes_test.rb +0 -7
  70. data/test/commands_on_hyper_log_log_test.rb +0 -7
  71. data/test/commands_on_lists_test.rb +0 -7
  72. data/test/commands_on_sets_test.rb +0 -7
  73. data/test/commands_on_sorted_sets_test.rb +0 -7
  74. data/test/commands_on_strings_test.rb +0 -7
  75. data/test/commands_on_value_types_test.rb +0 -207
  76. data/test/connection_handling_test.rb +0 -275
  77. data/test/connection_test.rb +0 -57
  78. data/test/distributed_blocking_commands_test.rb +0 -52
  79. data/test/distributed_commands_on_hashes_test.rb +0 -21
  80. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -26
  81. data/test/distributed_commands_on_lists_test.rb +0 -19
  82. data/test/distributed_commands_on_sets_test.rb +0 -105
  83. data/test/distributed_commands_on_sorted_sets_test.rb +0 -59
  84. data/test/distributed_commands_on_strings_test.rb +0 -79
  85. data/test/distributed_commands_on_value_types_test.rb +0 -129
  86. data/test/distributed_commands_requiring_clustering_test.rb +0 -162
  87. data/test/distributed_connection_handling_test.rb +0 -21
  88. data/test/distributed_internals_test.rb +0 -68
  89. data/test/distributed_key_tags_test.rb +0 -50
  90. data/test/distributed_persistence_control_commands_test.rb +0 -24
  91. data/test/distributed_publish_subscribe_test.rb +0 -90
  92. data/test/distributed_remote_server_control_commands_test.rb +0 -64
  93. data/test/distributed_scripting_test.rb +0 -100
  94. data/test/distributed_sorting_test.rb +0 -18
  95. data/test/distributed_test.rb +0 -56
  96. data/test/distributed_transactions_test.rb +0 -30
  97. data/test/encoding_test.rb +0 -14
  98. data/test/error_replies_test.rb +0 -57
  99. data/test/fork_safety_test.rb +0 -60
  100. data/test/helper.rb +0 -344
  101. data/test/helper_test.rb +0 -22
  102. data/test/internals_test.rb +0 -395
  103. data/test/lint/blocking_commands.rb +0 -174
  104. data/test/lint/hashes.rb +0 -203
  105. data/test/lint/hyper_log_log.rb +0 -74
  106. data/test/lint/lists.rb +0 -159
  107. data/test/lint/sets.rb +0 -282
  108. data/test/lint/sorted_sets.rb +0 -497
  109. data/test/lint/strings.rb +0 -348
  110. data/test/lint/value_types.rb +0 -130
  111. data/test/persistence_control_commands_test.rb +0 -24
  112. data/test/pipelining_commands_test.rb +0 -246
  113. data/test/publish_subscribe_test.rb +0 -280
  114. data/test/remote_server_control_commands_test.rb +0 -175
  115. data/test/scanning_test.rb +0 -407
  116. data/test/scripting_test.rb +0 -76
  117. data/test/sentinel_command_test.rb +0 -78
  118. data/test/sentinel_test.rb +0 -253
  119. data/test/sorting_test.rb +0 -57
  120. data/test/ssl_test.rb +0 -69
  121. data/test/support/cluster/orchestrator.rb +0 -199
  122. data/test/support/connection/hiredis.rb +0 -1
  123. data/test/support/connection/ruby.rb +0 -1
  124. data/test/support/connection/synchrony.rb +0 -17
  125. data/test/support/redis_mock.rb +0 -130
  126. data/test/support/ssl/gen_certs.sh +0 -31
  127. data/test/support/ssl/trusted-ca.crt +0 -25
  128. data/test/support/ssl/trusted-ca.key +0 -27
  129. data/test/support/ssl/trusted-cert.crt +0 -81
  130. data/test/support/ssl/trusted-cert.key +0 -28
  131. data/test/support/ssl/untrusted-ca.crt +0 -26
  132. data/test/support/ssl/untrusted-ca.key +0 -27
  133. data/test/support/ssl/untrusted-cert.crt +0 -82
  134. data/test/support/ssl/untrusted-cert.key +0 -28
  135. data/test/support/wire/synchrony.rb +0 -24
  136. data/test/support/wire/thread.rb +0 -5
  137. data/test/synchrony_driver.rb +0 -85
  138. data/test/test.conf.erb +0 -9
  139. data/test/thread_safety_test.rb +0 -60
  140. data/test/transactions_test.rb +0 -272
  141. data/test/unknown_commands_test.rb +0 -12
  142. data/test/url_param_test.rb +0 -136
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 07e1a92f8c30f083ff776facb54362ff789e63a4
4
- data.tar.gz: bf5cb573dcfb6c37f68bf9f903a161aec41ef8e3
3
+ metadata.gz: fb5e8fcd26f9729131009cc0c25bed4cdf1009f5
4
+ data.tar.gz: 9ed7be2cc83d01dcb6c69002680105dc544e659a
5
5
  SHA512:
6
- metadata.gz: afcd92eef3e98a0af75d7a9a850a6a44f133eae9fc84c6b83f2790eed9784445e738fac4174f814b72c45e137d257da362868b081a7ab1ef901e50d07196a4ed
7
- data.tar.gz: e933241b40c387e8bd3c702ceaee786b7b82e4baa8428be0255f99f6e3039ef9d4c8b3f2e68da0e7f823c70364ea662b6171fb95681b125b9990a3435ceab898
6
+ metadata.gz: b39badbb4689a4ea93cbc65ad00f0c967f24dadcc41e7750ab82977176236d6d8609d8e7b74e13a3829ce008c4a5de90930e265f6b6e03af710811419da6ccf6
7
+ data.tar.gz: 3ede92146cb181328657a4713e94473f86661dffe0a66d23f48edaa2c7a6c52543a7e7036794baa9dcc2945bc3f9e5eebd9192b70510bda1ad9c0d6e67253830
@@ -1,6 +1,17 @@
1
- # 4.1.0.beta1
1
+ # Unreleased
2
2
 
3
- * Added Redis Cluster support. See #716.
3
+ # 4.1.0
4
+
5
+ * Add Redis Cluster support. See #716.
6
+ * Add streams support. See #799 and #811.
7
+ * Add ZPOP* support. See #812.
8
+ * Fix issues with integer-like objects as BPOP timeout
9
+
10
+ # 4.0.3
11
+
12
+ * Fix raising command error for first command in pipeline. See #788.
13
+ * Fix the gemspec to stop exposing a `build` executable. See #785.
14
+ * Add `:reconnect_delay` and `:reconnect_delay_max` options. See #778.
4
15
 
5
16
  # 4.0.2
6
17
 
data/README.md CHANGED
@@ -220,6 +220,51 @@ end
220
220
 
221
221
  If no message is received after 5 seconds, the client will unsubscribe.
222
222
 
223
+ ## Reconnections
224
+
225
+ The client allows you to configure how many `reconnect_attempts` it should
226
+ complete before declaring a connection as failed. Furthermore, you may want
227
+ to control the maximum duration between reconnection attempts with
228
+ `reconnect_delay` and `reconnect_delay_max`.
229
+
230
+ ```ruby
231
+ Redis.new(
232
+ :reconnect_attempts => 10,
233
+ :reconnect_delay => 1.5,
234
+ :reconnect_delay_max => 10.0,
235
+ )
236
+ ```
237
+
238
+ The delay values are specified in seconds. With the above configuration, the
239
+ client would attempt 10 reconnections, exponentially increasing the duration
240
+ between each attempt but it never waits longer than `reconnect_delay_max`.
241
+
242
+ This is the retry algorithm:
243
+
244
+ ```ruby
245
+ attempt_wait_time = [(reconnect_delay * 2**(attempt-1)), reconnect_delay_max].min
246
+ ```
247
+
248
+ **By default**, this gem will only **retry a connection once** and then fail, but with the
249
+ above configuration the reconnection attempt would look like this:
250
+
251
+ #|Attempt wait time|Total wait time
252
+ :-:|:-:|:-:
253
+ 1|1.5s|1.5s
254
+ 2|3.0s|4.5s
255
+ 3|6.0s|10.5s
256
+ 4|10.0s|20.5s
257
+ 5|10.0s|30.5s
258
+ 6|10.0s|40.5s
259
+ 7|10.0s|50.5s
260
+ 8|10.0s|60.5s
261
+ 9|10.0s|70.5s
262
+ 10|10.0s|80.5s
263
+
264
+ So if the reconnection attempt #10 succeeds 70 seconds have elapsed trying
265
+ to reconnect, this is likely fine in long-running background processes, but if
266
+ you use Redis to drive your website you might want to have a lower
267
+ `reconnect_delay_max` or have less `reconnect_attempts`.
223
268
 
224
269
  ## SSL/TLS Support
225
270
 
@@ -499,22 +499,27 @@ class Redis
499
499
 
500
500
  # Transfer a key from the connected instance to another instance.
501
501
  #
502
- # @param [String] key
502
+ # @param [String, Array<String>] key
503
503
  # @param [Hash] options
504
504
  # - `:host => String`: host of instance to migrate to
505
505
  # - `:port => Integer`: port of instance to migrate to
506
506
  # - `:db => Integer`: database to migrate to (default: same as source)
507
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.
508
510
  # @return [String] `"OK"`
509
511
  def migrate(key, options)
510
- host = options[:host] || raise(RuntimeError, ":host not specified")
511
- port = options[:port] || raise(RuntimeError, ":port not specified")
512
- db = (options[:db] || @client.db).to_i
513
- 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)
514
521
 
515
- synchronize do |client|
516
- client.call([:migrate, host, port, key, db, timeout])
517
- end
522
+ synchronize { |client| client.call(args) }
518
523
  end
519
524
 
520
525
  # Delete one or more keys.
@@ -1151,15 +1156,14 @@ class Redis
1151
1156
  end
1152
1157
  end
1153
1158
 
1154
- def _bpop(cmd, args)
1159
+ def _bpop(cmd, args, &blk)
1155
1160
  options = {}
1156
1161
 
1157
- case args.last
1158
- when Hash
1162
+ if args.last.is_a?(Hash)
1159
1163
  options = args.pop
1160
- when Integer
1164
+ elsif args.last.respond_to?(:to_int)
1161
1165
  # Issue deprecation notice in obnoxious mode...
1162
- options[:timeout] = args.pop
1166
+ options[:timeout] = args.pop.to_int
1163
1167
  end
1164
1168
 
1165
1169
  if args.size > 1
@@ -1172,7 +1176,7 @@ class Redis
1172
1176
  synchronize do |client|
1173
1177
  command = [cmd, keys, timeout]
1174
1178
  timeout += client.timeout if timeout > 0
1175
- client.call_with_timeout(command, timeout)
1179
+ client.call_with_timeout(command, timeout, &blk)
1176
1180
  end
1177
1181
  end
1178
1182
 
@@ -1623,6 +1627,90 @@ class Redis
1623
1627
  end
1624
1628
  end
1625
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
+
1626
1714
  # Get the score associated with the given member in a sorted set.
1627
1715
  #
1628
1716
  # @example Get the score for member "a"
@@ -2803,6 +2891,345 @@ class Redis
2803
2891
  end
2804
2892
  end
2805
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
+
2806
3233
  # Interact with the sentinel command (masters, master, slaves, failover)
2807
3234
  #
2808
3235
  # @param [String] subcommand e.g. `masters`, `master`, `slaves`
@@ -2936,12 +3363,8 @@ private
2936
3363
 
2937
3364
  FloatifyPairs =
2938
3365
  lambda { |result|
2939
- if result.respond_to?(:each_slice)
2940
- result.each_slice(2).map do |member, score|
2941
- [member, Floatify.call(score)]
2942
- end
2943
- else
2944
- result
3366
+ result.each_slice(2).map do |member, score|
3367
+ [member, Floatify.call(score)]
2945
3368
  end
2946
3369
  }
2947
3370
 
@@ -2952,6 +3375,43 @@ private
2952
3375
  end.compact]
2953
3376
  }
2954
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
+ }
3412
+ end
3413
+ }
3414
+
2955
3415
  HashifyClusterNodeInfo =
2956
3416
  lambda { |str|
2957
3417
  arr = str.split(' ')
@@ -3017,6 +3477,23 @@ private
3017
3477
  @client = original
3018
3478
  end
3019
3479
  end
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
3020
3497
  end
3021
3498
 
3022
3499
  require_relative "redis/version"