redis-time-series 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4648678522434687a6e278490c7a308c64f6460394b8ebde16b2ce2daa9e92ad
4
- data.tar.gz: e2f48fb1c8efebb3b4d6e4c47d1d86137a10ac8a54735c94b3bdb35ef6396651
3
+ metadata.gz: 400fac8affe8b6c5acbc7892bd9704fb39da3135929451ce282ad43278f698ff
4
+ data.tar.gz: 70a10d9ca8cd01e8b113e6b769008ccb0e9a167cbd16777773d2df8d8a41d2c2
5
5
  SHA512:
6
- metadata.gz: 269a787d8d2a2a467a52e915ab717bc5b20eb28cacc55eb5dfc9ce3909a7d83cdd4e15b19afc92612d19fa551c6ba793df76ceef03d8bb8bdb16b6dc0253d2cc
7
- data.tar.gz: c5d845052ca8f2a518fb3a20a82a8d8aa35128cb824f54d2531a949767e90f8c7979ede07ba1bd56ae30a825edb9f6be90e52ca404a14199bfd13484026d8d40
6
+ metadata.gz: 7b166b6d74161936686e1f0d8d6d5b27238414036b16fc8c4537cc02a2848734b8bf26a183820069b7ad662d45465fc38e6fc1c307dbc543c028a746a30d5024
7
+ data.tar.gz: 3f3f178d267e261e9c3ee17b50fff7e5826d231bf9051364c070727af13841a6118596c255857bf750973445a9860cbbeaa007001425f602d409381dd24a851f
@@ -12,9 +12,10 @@ jobs:
12
12
  spec:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
+ fail-fast: false
15
16
  matrix:
16
17
  image_version: ['latest', 'edge']
17
- ruby_version: ['2.6', '2.7']
18
+ ruby_version: ['2.6', '2.7', '3.0', '3.1']
18
19
  services:
19
20
  redis:
20
21
  image: redislabs/redistimeseries:${{ matrix.image_version }}
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.0
6
+ * Add Ruby 3.1 to build matrix (#70)
7
+ * Add Ruby 3.0 to build matrix (#63)
8
+ * Relax Redis version constraint (#62)
9
+ * Add TS.REVRANGE, TS.MRANGE, and TS.MREVRANGE commands (#19)
10
+ * Update TS.MADD commands to consolidate parsing (#58)
11
+
5
12
  ## 0.6.0
6
13
  * Add CHUNK_SIZE param to CREATE, ADD, INCRBY, DECRBY commands (#53)
7
14
  * Add duplication policy to TS.CREATE and TS.ADD commands (#51)
data/README.md CHANGED
@@ -24,7 +24,7 @@ ts.add 56
24
24
  => #<Redis::TimeSeries::Sample:0x00007f8c0d26c460 @time=2020-06-25 23:23:16 -0700, @value=0.56e2>
25
25
  ts.add 78
26
26
  => #<Redis::TimeSeries::Sample:0x00007f8c0d276618 @time=2020-06-25 23:23:20 -0700, @value=0.78e2>
27
- ts.range (Time.now.to_i - 100)..Time.now.to_i
27
+ ts.range (Time.now.to_i - 100)..Time.now.to_i * 1000
28
28
  => [#<Redis::TimeSeries::Sample:0x00007f8c0d297200 @time=2020-06-25 23:23:04 -0700, @value=0.1234e4>,
29
29
  #<Redis::TimeSeries::Sample:0x00007f8c0d297048 @time=2020-06-25 23:23:16 -0700, @value=0.56e2>,
30
30
  #<Redis::TimeSeries::Sample:0x00007f8c0d296e90 @time=2020-06-25 23:23:20 -0700, @value=0.78e2>]
@@ -213,6 +213,38 @@ Redis::TimeSeries.where('foo=bar')
213
213
  @retention=nil,
214
214
  @uncompressed=false>]
215
215
  ```
216
+
217
+ ### Querying Multiple Series
218
+ Get all samples from matching series over a time range with `mrange`
219
+ ```ruby
220
+ [4] pry(main)> result = Redis::TimeSeries.mrange(1.minute.ago.., filter: { foo: 'bar' })
221
+ => [#<struct Redis::TimeSeries::Multi::Result
222
+ series=
223
+ #<Redis::TimeSeries:0x00007f833e408ad0
224
+ @key="ts3",
225
+ @redis=#<Redis client v4.2.5 for redis://127.0.0.1:6379/0>>,
226
+ labels=[],
227
+ samples=
228
+ [#<Redis::TimeSeries::Sample:0x00007f833e408a58
229
+ @time=2021-06-17 20:58:33 3246391/4194304 -0700,
230
+ @value=0.1e1>,
231
+ #<Redis::TimeSeries::Sample:0x00007f833e408850
232
+ @time=2021-06-17 20:58:33 413139/524288 -0700,
233
+ @value=0.3e1>,
234
+ #<Redis::TimeSeries::Sample:0x00007f833e408670
235
+ @time=2021-06-17 20:58:33 1679819/2097152 -0700,
236
+ @value=0.2e1>]>]
237
+ [5] pry(main)> result.keys
238
+ => ["ts3"]
239
+ [6] pry(main)> result['ts3'].values
240
+ => [0.1e1, 0.3e1, 0.2e1]
241
+ ```
242
+ Order them from newest to oldest with `mrevrange`
243
+ ```ruby
244
+ [8] pry(main)> Redis::TimeSeries.mrevrange(1.minute.ago.., filter: { foo: 'bar' }).first.values
245
+ => [0.2e1, 0.3e1, 0.1e1]
246
+ ```
247
+
216
248
  ### Filter DSL
217
249
  You can provide filter strings directly, per the time series documentation.
218
250
  ```ruby
@@ -299,11 +331,8 @@ ts.rules.first.delete
299
331
  Redis::TimeSeries.delete_rule(source: ts, dest: 'other_ts')
300
332
  ```
301
333
 
302
-
303
334
  ### TODO
304
- * `TS.REVRANGE`
305
- * `TS.MRANGE`/`TS.MREVRANGE`
306
- * Probably a bunch more stuff
335
+ * Check the [open issues](https://github.com/dzunk/redis-time-series/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) on Github.
307
336
 
308
337
  ## Development
309
338
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+ class Redis
3
+ class TimeSeries
4
+ # A {Multi} is a collection of multiple series and their samples, returned
5
+ # from a multi command (e.g. TS.MGET or TS.MRANGE).
6
+ #
7
+ # @see TimeSeries.mrange
8
+ # @see TimeSeries.mrevrange
9
+ class Multi < DelegateClass(Array)
10
+ # Multis are initialized by one of the class-level query commands.
11
+ # There's no need to ever create one yourself.
12
+ # @api private
13
+ def initialize(result_array)
14
+ super(result_array.map do |res|
15
+ Result.new(
16
+ TimeSeries.new(res[0]),
17
+ res[1],
18
+ res[2].map { |s| Sample.new(s[0], s[1]) }
19
+ )
20
+ end)
21
+ end
22
+
23
+ # Access a specific result by either array position, or hash lookup.
24
+ #
25
+ # @param index_or_key [Integer, String] The integer position, or series
26
+ # key, of the specific result to return.
27
+ # @return [Multi::Result, nil] A single series result, or nil if there is
28
+ # no matching series.
29
+ def [](index_or_key)
30
+ return super if index_or_key.is_a?(Integer)
31
+ find { |result| result.series.key == index_or_key.to_s }
32
+ end
33
+
34
+ # Get all the series keys that are present in this result collection.
35
+ # @return [Array<String>] An array of the series keys in these results.
36
+ def keys
37
+ map { |r| r.series.key }
38
+ end
39
+
40
+ # Get all the series objects that are present in this result collection.
41
+ # @return [Array<TimeSeries>] An array of the series in these results.
42
+ def series
43
+ map(&:series)
44
+ end
45
+
46
+ # Convert these results into a hash, keyed by series name.
47
+ # @return [Hash<Array>] A hash of series names and samples.
48
+ # @example
49
+ # {"ts3"=>
50
+ # [{:timestamp=>1623945216042, :value=>0.1e1},
51
+ # {:timestamp=>1623945216055, :value=>0.3e1},
52
+ # {:timestamp=>1623945216069, :value=>0.2e1}]}
53
+ def to_h
54
+ super do |result|
55
+ [result.series.key, result.samples.map(&:to_h)]
56
+ end
57
+ end
58
+
59
+ # Get a count of all matching samples from all series in this result collection.
60
+ # @return [Integer] The total size of all samples from all series in these results.
61
+ def sample_count
62
+ reduce(0) { |size, r| size += r.samples.size }
63
+ end
64
+
65
+ Result = Struct.new(:series, :labels, :samples) do
66
+ def values
67
+ samples.map(&:value)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class Redis
3
3
  class TimeSeries
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
6
6
  end
@@ -41,7 +41,8 @@ class Redis
41
41
  # A duplication policy to resolve conflicts when adding values to the series.
42
42
  # Valid values are in Redis::TimeSeries::DuplicatePolicy::VALID_POLICIES
43
43
  # @option options [Integer] :chunk_size
44
- # Amount of memory, in bytes, to allocate for each chunk of data
44
+ # Amount of memory, in bytes, to allocate for each chunk of data. Must be a multiple
45
+ # of 8. Default for a series is 4096.
45
46
  #
46
47
  # @return [Redis::TimeSeries] the created time series
47
48
  # @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
@@ -88,6 +89,23 @@ class Redis
88
89
  redis.del key
89
90
  end
90
91
 
92
+ # Add multiple values to multiple series.
93
+ #
94
+ # @example Adding multiple values with timestamps
95
+ # Redis::TimeSeries.madd(
96
+ # foo: { 2.minutes.ago => 123, 1.minute.ago => 456, Time.current => 789) },
97
+ # bar: { 2.minutes.ago => 987, 1.minute.ago => 654, Time.current => 321) }
98
+ # )
99
+ # @example Adding multiple values without timestamps
100
+ # Redis::TimeSeries.madd(foo: 1, bar: 2, baz: 3)
101
+ #
102
+ # @param data [Hash] A hash of key-value pairs, with the key being the name of
103
+ # the series, and the value being a single scalar value or a nested hash
104
+ # of timestamp => value pairs
105
+ # @return [Array<Sample, Redis::CommandError>] an array of the resulting samples
106
+ # added, or a CommandError if the sample in question could not be added to the
107
+ # series
108
+ #
91
109
  def madd(data)
92
110
  data.reduce([]) do |memo, (key, value)|
93
111
  memo << parse_madd_values(key, value)
@@ -98,6 +116,46 @@ class Redis
98
116
  end
99
117
  end
100
118
  end
119
+ alias multi_add madd
120
+ alias add_multiple madd
121
+
122
+ # Query across multiple series, returning values from oldest to newest.
123
+ #
124
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
125
+ # indicate oldest and most recent timestamp, respectively.
126
+ # @param filter [Hash, String] a set of filters to query with. Refer to the {Filters}
127
+ # documentation for more details on how to filter.
128
+ # @param count [Integer] The maximum number of results to return for each series.
129
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
130
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
131
+ # aggregation_type and duration +[:avg, 120000]+
132
+ # @param with_labels [Boolean] Whether to return the label details of the matched
133
+ # series in the result object.
134
+ # @return [Multi] A multi-series collection of results
135
+ #
136
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange
137
+ def mrange(range, filter:, count: nil, aggregation: nil, with_labels: false)
138
+ multi_cmd('TS.MRANGE', range, filter, count, aggregation, with_labels)
139
+ end
140
+
141
+ # Query across multiple series, returning values from newest to oldest.
142
+ #
143
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
144
+ # indicate oldest and most recent timestamp, respectively.
145
+ # @param filter [Hash, String] a set of filters to query with. Refer to the {Filters}
146
+ # documentation for more details on how to filter.
147
+ # @param count [Integer] The maximum number of results to return for each series.
148
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
149
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
150
+ # aggregation_type and duration +[:avg, 120000]+
151
+ # @param with_labels [Boolean] Whether to return the label details of the matched
152
+ # series in the result object.
153
+ # @return [Multi] A multi-series collection of results
154
+ #
155
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange
156
+ def mrevrange(range, filter:, count: nil, aggregation: nil, with_labels: false)
157
+ multi_cmd('TS.MREVRANGE', range, filter, count, aggregation, with_labels)
158
+ end
101
159
 
102
160
  # Search for a time series matching the provided filters. Refer to the {Filters} documentation
103
161
  # for more details on how to filter.
@@ -124,19 +182,36 @@ class Redis
124
182
 
125
183
  private
126
184
 
185
+ def multi_cmd(cmd_name, range, filter, count, agg, with_labels)
186
+ filters = Filters.new(filter)
187
+ filters.validate!
188
+ cmd(
189
+ cmd_name,
190
+ (range.begin || '-'),
191
+ (range.end || '+'),
192
+ (['COUNT', count] if count),
193
+ Aggregation.parse(agg)&.to_a,
194
+ ('WITHLABELS' if with_labels),
195
+ ['FILTER', filters.to_a]
196
+ ).then { |response| Multi.new(response) }
197
+ end
198
+
127
199
  def key_for(series_or_string)
128
200
  series_or_string.is_a?(self) ? series_or_string.key : series_or_string.to_s
129
201
  end
130
202
 
131
203
  def parse_madd_values(key, raw)
132
- if raw.is_a?(Hash) || (raw.is_a?(Array) && raw.first.is_a?(Array))
204
+ if raw.is_a? Hash
133
205
  # multiple timestamp => value pairs
134
206
  raw.map do |timestamp, value|
135
207
  [key, timestamp, value]
136
208
  end
137
209
  elsif raw.is_a? Array
138
- # single [timestamp, value]
139
- [key, raw.first, raw.last]
210
+ # multiple values, no timestamps
211
+ now = Time.now.ts_msec
212
+ raw.each_with_index.map do |value, index|
213
+ [key, now + index, value]
214
+ end
140
215
  else
141
216
  # single value, no timestamp
142
217
  [key, '*', raw]
@@ -301,32 +376,30 @@ class Redis
301
376
  cmd 'TS.ALTER', key, 'LABELS', val.to_a
302
377
  end
303
378
 
304
- def madd(*values)
305
- if values.one? && values.first.is_a?(Hash)
306
- # Hash of timestamp => value pairs
307
- args = values.first.map do |ts, val|
308
- [key, ts, val]
309
- end.flatten
310
- elsif values.one? && values.first.is_a?(Array)
311
- # Array of values, no timestamps
312
- initial_ts = Time.now.ts_msec
313
- args = values.first.each_with_index.map do |val, idx|
314
- [key, initial_ts + idx, val]
315
- end.flatten
316
- else
317
- # Values as individual arguments, no timestamps
318
- initial_ts = Time.now.ts_msec
319
- args = values.each_with_index.map do |val, idx|
320
- [key, initial_ts + idx, val]
321
- end.flatten
379
+ # Add multiple values to the series.
380
+ #
381
+ # @example Adding multiple values with timestamps
382
+ # ts.madd(2.minutes.ago => 987, 1.minute.ago => 654, Time.current => 321)
383
+ #
384
+ # @param data [Hash] A hash of key-value pairs, with the key being a Time
385
+ # object or integer timestamp, and the value being a single scalar value
386
+ # @return [Array<Sample, Redis::CommandError>] an array of the resulting samples
387
+ # added, or a CommandError if the sample in question could not be added to the
388
+ # series
389
+ #
390
+ def madd(data)
391
+ args = self.class.send(:parse_madd_values, key, data)
392
+ cmd('TS.MADD', args).each_with_index.map do |result, idx|
393
+ result.is_a?(Redis::CommandError) ? result : Sample.new(result, args[idx][2])
322
394
  end
323
- # TODO: return Sample objects here
324
- cmd 'TS.MADD', args
325
395
  end
396
+ alias multi_add madd
397
+ alias add_multiple madd
326
398
 
327
- # Get a range of values from the series
399
+ # Get a range of values from the series, from earliest to most recent
328
400
  #
329
- # @param range [Hash, Range] a time range, or hash of +from+ and +to+ values
401
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
402
+ # indicate oldest and most recent timestamp, respectively.
330
403
  # @param count [Integer] the maximum number of results to return
331
404
  # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
332
405
  # The aggregation to apply. Can be an {Aggregation} object, or an array of
@@ -336,20 +409,23 @@ class Redis
336
409
  #
337
410
  # @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
338
411
  def range(range, count: nil, aggregation: nil)
339
- if range.is_a?(Hash)
340
- # This is to support from: and to: passed in as hash keys
341
- # `range` will swallow all parameters if they're all hash syntax
342
- count = range.delete(:count)
343
- aggregation = range.delete(:aggregation)
344
- range = range.fetch(:from)..range[:to]
345
- end
346
- cmd('TS.RANGE',
347
- key,
348
- (range.begin || '-'),
349
- (range.end || '+'),
350
- (['COUNT', count] if count),
351
- Aggregation.parse(aggregation)&.to_a
352
- ).map { |ts, val| Sample.new(ts, val) }
412
+ range_cmd('TS.RANGE', range, count, aggregation)
413
+ end
414
+
415
+ # Get a range of values from the series, from most recent to earliest
416
+ #
417
+ # @param range [Range] A time range over which to query. Beginless and endless ranges
418
+ # indicate oldest and most recent timestamp, respectively.
419
+ # @param count [Integer] the maximum number of results to return
420
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
421
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
422
+ # aggregation_type and duration +[:avg, 120000]+
423
+ #
424
+ # @return [Array<Sample>] an array of samples matching the range query
425
+ #
426
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
427
+ def revrange(range, count: nil, aggregation: nil)
428
+ range_cmd('TS.REVRANGE', range, count, aggregation)
353
429
  end
354
430
 
355
431
  # Set data retention time for the series using +TS.ALTER+
@@ -369,5 +445,17 @@ class Redis
369
445
  return false unless other.is_a?(self.class)
370
446
  key == other.key && redis == other.redis
371
447
  end
448
+
449
+ private
450
+
451
+ def range_cmd(cmd_name, range, count, agg)
452
+ cmd(cmd_name,
453
+ key,
454
+ (range.begin || '-'),
455
+ (range.end || '+'),
456
+ (['COUNT', count] if count),
457
+ Aggregation.parse(agg)&.to_a
458
+ ).map { |ts, val| Sample.new(ts, val) }
459
+ end
372
460
  end
373
461
  end
@@ -7,6 +7,7 @@ require 'redis/time_series/errors'
7
7
  require 'redis/time_series/aggregation'
8
8
  require 'redis/time_series/duplicate_policy'
9
9
  require 'redis/time_series/filters'
10
+ require 'redis/time_series/multi'
10
11
  require 'redis/time_series/rule'
11
12
  require 'redis/time_series/info'
12
13
  require 'redis/time_series/sample'
@@ -31,10 +31,10 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
32
32
  spec.require_paths = ['lib']
33
33
 
34
- spec.add_dependency 'redis', '>= 3.3', '< 5'
34
+ spec.add_dependency 'redis', '>= 3.3'
35
35
 
36
36
  spec.add_development_dependency 'activesupport', '~> 6.0'
37
- spec.add_development_dependency 'appraisal'
37
+ spec.add_development_dependency 'appraisal', '>= 2.4.1'
38
38
  spec.add_development_dependency 'bundler', '~> 2.0'
39
39
  spec.add_development_dependency 'pry', '~> 0.13'
40
40
  spec.add_development_dependency 'rake', '~> 13.0'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-time-series
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Duszynski
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-12-20 00:00:00.000000000 Z
11
+ date: 2022-01-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -17,9 +17,6 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.3'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: '5'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
@@ -27,9 +24,6 @@ dependencies:
27
24
  - - ">="
28
25
  - !ruby/object:Gem::Version
29
26
  version: '3.3'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: '5'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: activesupport
35
29
  requirement: !ruby/object:Gem::Requirement
@@ -50,14 +44,14 @@ dependencies:
50
44
  requirements:
51
45
  - - ">="
52
46
  - !ruby/object:Gem::Version
53
- version: '0'
47
+ version: 2.4.1
54
48
  type: :development
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
51
  requirements:
58
52
  - - ">="
59
53
  - !ruby/object:Gem::Version
60
- version: '0'
54
+ version: 2.4.1
61
55
  - !ruby/object:Gem::Dependency
62
56
  name: bundler
63
57
  requirement: !ruby/object:Gem::Requirement
@@ -155,6 +149,7 @@ files:
155
149
  - lib/redis/time_series/errors.rb
156
150
  - lib/redis/time_series/filters.rb
157
151
  - lib/redis/time_series/info.rb
152
+ - lib/redis/time_series/multi.rb
158
153
  - lib/redis/time_series/rule.rb
159
154
  - lib/redis/time_series/sample.rb
160
155
  - lib/redis/time_series/version.rb
@@ -181,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
176
  - !ruby/object:Gem::Version
182
177
  version: '0'
183
178
  requirements: []
184
- rubygems_version: 3.2.2
179
+ rubygems_version: 3.2.22
185
180
  signing_key:
186
181
  specification_version: 4
187
182
  summary: A Ruby adapter for the RedisTimeSeries module.