redis-time-series 0.3.0 → 0.6.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: 0d699daec01bb891952501de6a51cc4b966be74a0f8585030bafb4f6a908b36e
4
- data.tar.gz: dfa027d344c4d5485b0a22a33edd67973c2a60cb60dbbaa1a57101ccce8833fc
3
+ metadata.gz: 4648678522434687a6e278490c7a308c64f6460394b8ebde16b2ce2daa9e92ad
4
+ data.tar.gz: e2f48fb1c8efebb3b4d6e4c47d1d86137a10ac8a54735c94b3bdb35ef6396651
5
5
  SHA512:
6
- metadata.gz: acadcad9c6919e810f0e100bfe20b60055507e2efa98d0853fca6c15a252f99b65c722c71acc99d37d8d9f72964d91f862c9604fb96458ca3faaddce9f841752
7
- data.tar.gz: 489415e8247d8cffefcf13e4e70c82cde752170c4416bc0973495c5d38b1d27d2b554cdd62db8729c9c71b125cb4848aa5a1efd81b998aa5f6e5683c13be8ccc
6
+ metadata.gz: 269a787d8d2a2a467a52e915ab717bc5b20eb28cacc55eb5dfc9ce3909a7d83cdd4e15b19afc92612d19fa551c6ba793df76ceef03d8bb8bdb16b6dc0253d2cc
7
+ data.tar.gz: c5d845052ca8f2a518fb3a20a82a8d8aa35128cb824f54d2531a949767e90f8c7979ede07ba1bd56ae30a825edb9f6be90e52ca404a14199bfd13484026d8d40
@@ -11,18 +11,39 @@ on:
11
11
  jobs:
12
12
  spec:
13
13
  runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ image_version: ['latest', 'edge']
17
+ ruby_version: ['2.6', '2.7']
14
18
  services:
15
19
  redis:
16
- image: redislabs/redistimeseries:latest
20
+ image: redislabs/redistimeseries:${{ matrix.image_version }}
17
21
  ports:
18
22
  - 6379:6379/tcp
23
+ env:
24
+ CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
25
+ GIT_COMMIT_SHA: ${{ github.sha }}
26
+ GIT_BRANCH: ${{ github.head_ref }}
19
27
  steps:
20
28
  - uses: actions/checkout@v2
21
29
  - name: Set up Ruby
22
30
  uses: ruby/setup-ruby@v1
23
31
  with:
24
- ruby-version: 2.6
32
+ ruby-version: ${{ matrix.ruby_version }}
33
+ - name: Set up CodeClimate
34
+ run: |
35
+ curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
36
+ chmod +x ./cc-test-reporter
37
+ ./cc-test-reporter before-build
25
38
  - name: Install dependencies
26
- run: bundle install
39
+ run: |
40
+ bundle install
41
+ bundle exec appraisal install
27
42
  - name: Run specs
28
- run: bundle exec rake spec
43
+ run: bundle exec appraisal rake spec
44
+ - name: Upload coverage report
45
+ run: ./cc-test-reporter after-build -t simplecov coverage/.resultset.json
46
+ - uses: actions/upload-artifact@v2
47
+ with:
48
+ name: coverage
49
+ path: coverage/
data/.gitignore CHANGED
@@ -6,6 +6,7 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ Gemfile.lock
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
@@ -0,0 +1,7 @@
1
+ appraise 'redis 3' do
2
+ gem 'redis', '~> 3.3'
3
+ end
4
+
5
+ appraise 'redis 4' do
6
+ gem 'redis', '~> 4.0'
7
+ end
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.0
6
+ * Add CHUNK_SIZE param to CREATE, ADD, INCRBY, DECRBY commands (#53)
7
+ * Add duplication policy to TS.CREATE and TS.ADD commands (#51)
8
+ * Add support for endless ranges to TS.RANGE (#50)
9
+ * Cast label values to integers in Info struct (#49)
10
+ * Build against edge upstream in addition to latest stable (#48)
11
+
12
+ ## 0.5.2
13
+ * Add chunk_type to info struct (#47)
14
+
15
+ ## 0.5.1
16
+ * Update Info struct for RTS 1.4 compatibility (#45)
17
+
18
+ ## 0.5.0
19
+ * Fix aggregations for TS.RANGE command (#34)
20
+ * Extract client handling into Client module (#32)
21
+ * Add `uncompressed` param to TS.ADD, TS.INCRBY, TS.DECRBY (#35)
22
+ * Add `Redis::TimeSeries::Rule` object (#38)
23
+ * Add [YARD documentation](https://rubydoc.info/gems/redis-time-series) (#40)
24
+
25
+ ## 0.4.0
26
+ * Added [hash-based filter DSL](https://github.com/dzunk/redis-time-series/tree/7173c73588da50614c02f9c89bf2ecef77766a78#filter-dsl)
27
+ * Removed `Time#ts_msec` monkey-patch
28
+ * Renamed `TimeSeries.queryindex` to `.query_index`
29
+ * Added `TS.CREATERULE` and `TS.DELETERULE` commands
30
+ * Renamed `InvalidFilters` to `FilterError`
31
+
5
32
  ## 0.3.0
6
33
  * Added `TS.QUERYINDEX` command
7
34
 
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  [![RSpec](https://github.com/dzunk/redis-time-series/workflows/RSpec/badge.svg)](https://github.com/dzunk/redis-time-series/actions?query=workflow%3ARSpec+branch%3Amaster)
2
2
  [![Gem Version](https://badge.fury.io/rb/redis-time-series.svg)](https://badge.fury.io/rb/redis-time-series)
3
+ [![Documentation](https://img.shields.io/badge/docs-rubydoc.info-brightgreen)](https://rubydoc.info/gems/redis-time-series)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/19a5925c20318508b4a4/maintainability)](https://codeclimate.com/github/dzunk/redis-time-series/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/19a5925c20318508b4a4/test_coverage)](https://codeclimate.com/github/dzunk/redis-time-series/test_coverage)
3
6
 
4
7
  # RedisTimeSeries
5
8
 
@@ -73,6 +76,10 @@ Add a single value with a timestamp
73
76
  ```ruby
74
77
  ts.add 1234, 3.minutes.ago # Used ActiveSupport here, but any Time object works fine
75
78
  => #<Redis::TimeSeries::Sample:0x00007fa6ce05f3f8 @time=2020-06-25 23:39:54 -0700, @value=0.1234e4>
79
+
80
+ # Optionally store data uncompressed
81
+ ts.add 5678, uncompressed: true
82
+ => #<Redis::TimeSeries::Sample:0x00007f93f43cdf68 @time=2020-07-18 23:15:29 -0700, @value=0.5678e4>
76
83
  ```
77
84
  Add multiple values with timestamps
78
85
  ```ruby
@@ -89,6 +96,10 @@ ts.increment # alias of incrby
89
96
  => 1593154255069
90
97
  ts.decrement # alias of decrby
91
98
  => 1593154257344
99
+
100
+ # Optionally store data uncompressed
101
+ ts.incrby 4, uncompressed: true
102
+ => 1595139299769
92
103
  ```
93
104
  ```ruby
94
105
  ts.get
@@ -120,31 +131,53 @@ ts.get
120
131
  ```
121
132
  Get a range of values
122
133
  ```ruby
123
- ts.range 10.minutes.ago..Time.current # Time range as an argument
134
+ # Time range as an argument
135
+ ts.range(10.minutes.ago..Time.current)
124
136
  => [#<Redis::TimeSeries::Sample:0x00007fa25f13fc28 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
125
- #<Redis::TimeSeries::Sample:0x00007fa25f13db58 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
126
- #<Redis::TimeSeries::Sample:0x00007fa25f13d900 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
127
- #<Redis::TimeSeries::Sample:0x00007fa25f13d680 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
128
- ts.range from: 10.minutes.ago, to: Time.current # Time range as keyword args
137
+ #<Redis::TimeSeries::Sample:0x00007fa25f13db58 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
138
+ #<Redis::TimeSeries::Sample:0x00007fa25f13d900 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
139
+ #<Redis::TimeSeries::Sample:0x00007fa25f13d680 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
140
+
141
+ # Time range as keyword args
142
+ ts.range(from: 10.minutes.ago, to: Time.current)
143
+ => [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
144
+ #<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
145
+ #<Redis::TimeSeries::Sample:0x00007fa25dc01b68 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
146
+ #<Redis::TimeSeries::Sample:0x00007fa25dc019b0 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
147
+
148
+ # Limit number of results with count argument
149
+ ts.range(10.minutes.ago..Time.current, count: 2)
129
150
  => [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:51 -0700, @value=0.57e2>,
130
- #<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>,
131
- #<Redis::TimeSeries::Sample:0x00007fa25dc01b68 @time=2020-06-25 23:50:57 -0700, @value=0.57e2>,
132
- #<Redis::TimeSeries::Sample:0x00007fa25dc019b0 @time=2020-06-25 23:51:30 -0700, @value=0.58e2>]
151
+ #<Redis::TimeSeries::Sample:0x00007fa25dc01d20 @time=2020-06-25 23:50:55 -0700, @value=0.58e2>]
152
+
153
+ # Apply aggregations to the range
154
+ ts.range(from: 10.minutes.ago, to: Time.current, aggregation: [:avg, 10.minutes])
155
+ => [#<Redis::TimeSeries::Sample:0x00007fa25dc01f00 @time=2020-06-25 23:50:00 -0700, @value=0.575e2>]
133
156
  ```
134
157
  Get info about the series
135
158
  ```ruby
136
159
  ts.info
137
160
  => #<struct Redis::TimeSeries::Info
161
+ series=
162
+ #<Redis::TimeSeries:0x00007ff46da9b578 @key="ts3", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>,
138
163
  total_samples=3,
139
- memory_usage=4184,
140
- first_timestamp=1594060993011,
141
- last_timestamp=1594060993060,
164
+ memory_usage=4264,
165
+ first_timestamp=1595187993605,
166
+ last_timestamp=1595187993629,
142
167
  retention_time=0,
143
168
  chunk_count=1,
144
169
  max_samples_per_chunk=256,
145
170
  labels={"foo"=>"bar"},
146
171
  source_key=nil,
147
- rules=[]>
172
+ rules=
173
+ [#<Redis::TimeSeries::Rule:0x00007ff46db30c68
174
+ @aggregation=#<Redis::TimeSeries::Aggregation:0x00007ff46db30c18 @duration=3600000, @type="avg">,
175
+ @destination_key="ts1",
176
+ @source=
177
+ #<Redis::TimeSeries:0x00007ff46da9b578
178
+ @key="ts3",
179
+ @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>>]>
180
+
148
181
  # Each info property is also a method on the time series object
149
182
  ts.memory_usage
150
183
  => 4208
@@ -152,6 +185,7 @@ ts.labels
152
185
  => {"foo"=>"bar"}
153
186
  ts.total_samples
154
187
  => 3
188
+
155
189
  # Total samples also available as #count, #length, and #size
156
190
  ts.count
157
191
  => 3
@@ -162,23 +196,113 @@ ts.size
162
196
  ```
163
197
  Find series matching specific label(s)
164
198
  ```ruby
165
- Redis::TimeSeries.queryindex('foo=bar')
199
+ Redis::TimeSeries.query_index('foo=bar')
166
200
  => [#<Redis::TimeSeries:0x00007fc115ba1610
167
201
  @key="ts3",
168
202
  @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>,
169
203
  @retention=nil,
170
204
  @uncompressed=false>]
171
205
  # Note that you need at least one "label equals value" filter
172
- Redis::TimeSeries.queryindex('foo!=bar')
206
+ Redis::TimeSeries.query_index('foo!=bar')
173
207
  => RuntimeError: Filtering requires at least one equality comparison
208
+ # query_index is also aliased as .where for fluency
209
+ Redis::TimeSeries.where('foo=bar')
210
+ => [#<Redis::TimeSeries:0x00007fb8981010c8
211
+ @key="ts3",
212
+ @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>,
213
+ @retention=nil,
214
+ @uncompressed=false>]
215
+ ```
216
+ ### Filter DSL
217
+ You can provide filter strings directly, per the time series documentation.
218
+ ```ruby
219
+ Redis::TimeSeries.where('foo=bar')
220
+ => [#<Redis::TimeSeries:0x00007fb8981010c8...>]
221
+ ```
222
+ There is also a hash-based syntax available, which may be more pleasant to work with.
223
+ ```ruby
224
+ Redis::TimeSeries.where(foo: 'bar')
225
+ => [#<Redis::TimeSeries:0x00007fb89811dca0...>]
226
+ ```
227
+ All six filter types are represented in hash format below.
228
+ ```ruby
229
+ {
230
+ foo: 'bar', # label=value (equality)
231
+ foo: { not: 'bar' }, # label!=value (inequality)
232
+ foo: true, # label= (presence)
233
+ foo: false, # label!= (absence)
234
+ foo: [1, 2], # label=(1,2) (any value)
235
+ foo: { not: [1, 2] } # label!=(1,2) (no values)
236
+ }
237
+ ```
238
+ Note the special use of `true` and `false`. If you're representing a boolean value with a label, rather than setting its value to "true" or "false" (which would be treated as strings in Redis anyway), you should add or remove the label from the series.
239
+
240
+ Values can be any object that responds to `.to_s`:
241
+ ```ruby
242
+ class Person
243
+ def initialize(name)
244
+ @name = name
245
+ end
246
+
247
+ def to_s
248
+ @name
249
+ end
250
+ end
251
+
252
+ Redis::TimeSeries.where(person: Person.new('John'))
253
+ #=> TS.QUERYINDEX person=John
254
+ ```
255
+
256
+ ### Compaction Rules
257
+ Add a compaction rule to a series.
258
+ ```ruby
259
+ # Destintation time series needs to be created before the rule is added.
260
+ other_ts = Redis::TimeSeries.create('other_ts')
261
+
262
+ # Aggregation buckets are measured in milliseconds
263
+ ts.create_rule(dest: other_ts, aggregation: [:count, 60000]) # 1 minute
264
+
265
+ # Can provide a string key instead of a time series object
266
+ ts.create_rule(dest: 'other_ts', aggregation: [:avg, 120000])
267
+
268
+ # If you're using Rails or ActiveSupport, you can provide an
269
+ # ActiveSupport::Duration instead of an integer
270
+ ts.create_rule(dest: other_ts, aggregation: [:avg, 2.minutes])
271
+
272
+ # Can also provide an Aggregation object instead of an array
273
+ agg = Redis::TimeSeries::Aggregation.new(:avg, 120000)
274
+ ts.create_rule(dest: other_ts, aggregation: agg)
275
+
276
+ # Class-level method also available
277
+ Redis::TimeSeries.create_rule(source: ts, dest: other_ts, aggregation: ['std.p', 150000])
278
+ ```
279
+ Get existing compaction rules
280
+ ```ruby
281
+ ts.rules
282
+ => [#<Redis::TimeSeries::Rule:0x00007ff46e91c728
283
+ @aggregation=#<Redis::TimeSeries::Aggregation:0x00007ff46e91c6d8 @duration=3600000, @type="avg">,
284
+ @destination_key="ts1",
285
+ @source=
286
+ #<Redis::TimeSeries:0x00007ff46da9b578 @key="ts3", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>>]
287
+
288
+ # Get properties of a rule too
289
+ ts.rules.first.aggregation
290
+ => #<Redis::TimeSeries::Aggregation:0x00007ff46d146d38 @duration=3600000, @type="avg">
291
+ ts.rules.first.destination
292
+ => #<Redis::TimeSeries:0x00007ff46d8a3d60 @key="ts1", @redis=#<Redis client v4.2.1 for redis://127.0.0.1:6379/0>>
293
+ ```
294
+
295
+ Remove an existing compaction rule
296
+ ```ruby
297
+ ts.delete_rule(dest: 'other_ts')
298
+ ts.rules.first.delete
299
+ Redis::TimeSeries.delete_rule(source: ts, dest: 'other_ts')
174
300
  ```
175
301
 
176
302
 
177
303
  ### TODO
178
304
  * `TS.REVRANGE`
179
305
  * `TS.MRANGE`/`TS.MREVRANGE`
180
- * Compaction rules
181
- * Filters
182
306
  * Probably a bunch more stuff
183
307
 
184
308
  ## Development
data/bin/setup CHANGED
@@ -3,10 +3,12 @@
3
3
  system 'bundle install'
4
4
  system 'docker pull redislabs/redistimeseries:latest'
5
5
  container_id = `docker run -p 6379:6379 -dit --rm redislabs/redistimeseries`
6
+ at_exit { system "docker stop #{container_id}" }
6
7
 
7
8
  require 'bundler/setup'
8
9
  require 'active_support/core_ext/numeric/time'
9
10
  require 'redis'
11
+ require 'redis-time-series'
10
12
 
11
13
  Redis.current.flushall
12
14
  ts1 = Redis::TimeSeries.create('ts1')
@@ -26,5 +28,4 @@ ts3.incrby 2
26
28
  sleep 0.01
27
29
  ts3.decrement
28
30
 
29
- at_exit { system "docker stop #{container_id}" }
30
31
  system "docker logs -f #{container_id}"
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The +TimeMsec+ module is a refinement for the +Time+ class that makes it easier
4
+ # to work with millisecond timestamps.
5
+ #
6
+ # @example
7
+ # Time.now.to_i # 1595194259
8
+ # Time.now.ts_msec # NoMethodError
9
+ #
10
+ # using TimeMsec
11
+ #
12
+ # Time.now.to_i # 1595194259
13
+ # Time.now.ts_msec # 1595194259000
14
+ #
15
+ # Time.from_msec(1595194259000) # 2020-07-19 14:30:59 -0700
16
+ module TimeMsec
17
+ refine Time do
18
+ # TODO: convert to #to_msec
19
+ def ts_msec
20
+ (to_f * 1000.0).to_i
21
+ end
22
+ end
23
+
24
+ refine Time.singleton_class do
25
+ def from_msec(timestamp)
26
+ at(timestamp / 1000.0)
27
+ end
28
+ end
29
+ end
@@ -1,11 +1,15 @@
1
1
  require 'bigdecimal'
2
2
  require 'forwardable'
3
- require 'time/msec'
4
- require 'redis/time_series/filter'
3
+ require 'ext/time_msec'
4
+
5
+ require 'redis/time_series/client'
6
+ require 'redis/time_series/errors'
7
+ require 'redis/time_series/aggregation'
8
+ require 'redis/time_series/duplicate_policy'
9
+ require 'redis/time_series/filters'
10
+ require 'redis/time_series/rule'
5
11
  require 'redis/time_series/info'
6
12
  require 'redis/time_series/sample'
7
13
  require 'redis/time_series'
8
14
 
9
- class RedisTimeSeries
10
- VERSION = '0.3.0'
11
- end
15
+ class RedisTimeSeries; end
@@ -1,88 +1,257 @@
1
1
  # frozen_string_literal: true
2
+ using TimeMsec
3
+
2
4
  class Redis
5
+ # The +Redis::TimeSeries+ class is an interface for working with time-series data in
6
+ # Redis, using the {https://oss.redislabs.com/redistimeseries RedisTimeSeries} module.
7
+ #
8
+ # You can't use this gem with vanilla Redis, the time series module must be compiled
9
+ # and loaded. The easiest way to do this is by running the provided Docker container.
10
+ # Refer to the {https://oss.redislabs.com/redistimeseries/#setup setup guide} for more info.
11
+ #
12
+ # +docker run -p 6379:6379 -it --rm redislabs/redistimeseries+
13
+ #
14
+ # Once you're up and running, you can create a new time series and start recording data.
15
+ # Many commands are documented below, but you should refer to the
16
+ # {https://oss.redislabs.com/redistimeseries/commands command documentation} for the most
17
+ # authoritative and up-to-date reference.
18
+ #
19
+ # @example
20
+ # ts = Redis::TimeSeries.create('time_series_example')
21
+ # ts.add(12345)
22
+ # ts.get
23
+ # #=> #<Redis::TimeSeries::Sample:0x00007ff00d942e60 @time=2020-07-19 16:52:48 -0700, @value=0.12345e5>
3
24
  class TimeSeries
25
+ extend Client
4
26
  extend Forwardable
5
27
 
6
28
  class << self
29
+ # Create a new time series.
30
+ #
31
+ # @param key [String] the Redis key to store time series data in
32
+ # @option options [Hash] :labels
33
+ # A hash of label-value pairs to apply to this series.
34
+ # @option options [Redis] :redis (self.class.redis) a different Redis client to use
35
+ # @option options [Integer] :retention
36
+ # Maximum age for samples compared to last event time (in milliseconds).
37
+ # With no value, the series will not be trimmed.
38
+ # @option options [Boolean] :uncompressed
39
+ # When true, series data will be stored in an uncompressed format.
40
+ # @option options [String, Symbol] :duplicate_policy
41
+ # A duplication policy to resolve conflicts when adding values to the series.
42
+ # Valid values are in Redis::TimeSeries::DuplicatePolicy::VALID_POLICIES
43
+ # @option options [Integer] :chunk_size
44
+ # Amount of memory, in bytes, to allocate for each chunk of data
45
+ #
46
+ # @return [Redis::TimeSeries] the created time series
47
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
7
48
  def create(key, **options)
8
- new(key, **options).create(labels: options[:labels])
49
+ new(key, redis: options.fetch(:redis, redis)).create(**options)
50
+ end
51
+
52
+ # Create a compaction rule for a series. Note that both source and destination series
53
+ # must exist before the rule can be created.
54
+ #
55
+ # @param source [String, TimeSeries] the source series (or key) to apply the rule to
56
+ # @param dest [String, TimeSeries] the destination series (or key) to aggregate the data
57
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
58
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
59
+ # aggregation_type and duration +[:avg, 120000]+
60
+ #
61
+ # @return [String] the string "OK"
62
+ # @raise [Redis::TimeSeries::AggregationError] if the given aggregation params are invalid
63
+ # @raise [Redis::CommandError] if the compaction rule cannot be applied to either series
64
+ #
65
+ # @see TimeSeries#create_rule
66
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tscreaterule
67
+ def create_rule(source:, dest:, aggregation:)
68
+ cmd 'TS.CREATERULE', key_for(source), key_for(dest), Aggregation.parse(aggregation).to_a
69
+ end
70
+
71
+ # Delete an existing compaction rule.
72
+ #
73
+ # @param source [String, TimeSeries] the source series (or key) to remove the rule from
74
+ # @param dest [String, TimeSeries] the destination series (or key) the rule applies to
75
+ #
76
+ # @return [String] the string "OK"
77
+ # @raise [Redis::CommandError] if the compaction rule does not exist
78
+ def delete_rule(source:, dest:)
79
+ cmd 'TS.DELETERULE', key_for(source), key_for(dest)
80
+ end
81
+
82
+ # Delete all data and remove a time series from Redis.
83
+ #
84
+ # @param key [String] the key to remove
85
+ # @return [1] if the series existed
86
+ # @return [0] if the series did not exist
87
+ def destroy(key)
88
+ redis.del key
9
89
  end
10
90
 
11
91
  def madd(data)
12
92
  data.reduce([]) do |memo, (key, value)|
13
- if value.is_a?(Hash) || (value.is_a?(Array) && value.first.is_a?(Array))
14
- # multiple timestamp => value pairs
15
- value.each do |timestamp, nested_value|
16
- timestamp = timestamp.ts_msec if timestamp.is_a? Time
17
- memo << [key, timestamp, nested_value]
18
- end
19
- elsif value.is_a? Array
20
- # single [timestamp, value]
21
- key = key.ts_msec if key.is_a? Time
22
- memo << [key, value]
23
- else
24
- # single value, no timestamp
25
- memo << [key, '*', value]
26
- end
93
+ memo << parse_madd_values(key, value)
27
94
  memo
28
95
  end.then do |args|
29
- puts "DEBUG: TS.MADD #{args.join(' ')}" if ENV['DEBUG']
30
- redis.call('TS.MADD', args.flatten).each_with_index.map do |result, idx|
96
+ cmd('TS.MADD', args).each_with_index.map do |result, idx|
31
97
  result.is_a?(Redis::CommandError) ? result : Sample.new(result, args[idx][2])
32
98
  end
33
99
  end
34
100
  end
35
101
 
36
- def queryindex(filter_value)
37
- filters = Filter.new(filter_value)
102
+ # Search for a time series matching the provided filters. Refer to the {Filters} documentation
103
+ # for more details on how to filter.
104
+ #
105
+ # @example Using a filter string
106
+ # Redis::TimeSeries.query_index('foo=bar')
107
+ # #=> [#<Redis::TimeSeries:0x00007ff00e222788 @key="ts3", @redis=#<Redis...>>]
108
+ # @example Using the .where alias with hash DSL
109
+ # Redis::TimeSeries.where(foo: 'bar')
110
+ # #=> [#<Redis::TimeSeries:0x00007ff00e2a1d30 @key="ts3", @redis=#<Redis...>>]
111
+ #
112
+ # @param filter_value [Hash, String] a set of filters to query with
113
+ # @return [Array<TimeSeries>] an array of series that matched the given filters
114
+ #
115
+ # @see Filters
116
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsqueryindex
117
+ # @see https://oss.redislabs.com/redistimeseries/commands/#filtering
118
+ def query_index(filter_value)
119
+ filters = Filters.new(filter_value)
38
120
  filters.validate!
39
- redis.call('TS.QUERYINDEX', *filters.to_a).map { |key| new(key) }
121
+ cmd('TS.QUERYINDEX', filters.to_a).map { |key| new(key) }
40
122
  end
123
+ alias where query_index
124
+
125
+ private
41
126
 
42
- def redis
43
- @redis ||= Redis.current
127
+ def key_for(series_or_string)
128
+ series_or_string.is_a?(self) ? series_or_string.key : series_or_string.to_s
44
129
  end
45
130
 
46
- def redis=(client)
47
- @redis = redis
131
+ def parse_madd_values(key, raw)
132
+ if raw.is_a?(Hash) || (raw.is_a?(Array) && raw.first.is_a?(Array))
133
+ # multiple timestamp => value pairs
134
+ raw.map do |timestamp, value|
135
+ [key, timestamp, value]
136
+ end
137
+ elsif raw.is_a? Array
138
+ # single [timestamp, value]
139
+ [key, raw.first, raw.last]
140
+ else
141
+ # single value, no timestamp
142
+ [key, '*', raw]
143
+ end
48
144
  end
49
145
  end
50
146
 
51
- attr_reader :key, :redis, :retention, :uncompressed
147
+ # @return [String] the Redis key this time series is stored in
148
+ attr_reader :key
52
149
 
53
- def initialize(key, options = {})
150
+ # @param key [String] the Redis key to store the time series in
151
+ # @param redis [Redis] an optional Redis client
152
+ def initialize(key, redis: self.class.redis)
54
153
  @key = key
55
- @redis = options[:redis] || self.class.redis
56
- @retention = options[:retention]
57
- @uncompressed = options[:uncompressed] || false
154
+ @redis = redis
58
155
  end
59
156
 
60
- def add(value, timestamp = '*')
61
- timestamp = timestamp.ts_msec if timestamp.is_a? Time
62
- ts = cmd 'TS.ADD', key, timestamp, value
157
+ # Add a value to the series.
158
+ #
159
+ # @param value [Numeric] the value to add
160
+ # @param timestamp [Time, Numeric] the +Time+, or integer timestamp in milliseconds, to add the value
161
+ # @param uncompressed [Boolean] if true, stores data in an uncompressed format
162
+ # @param on_duplicate [String, Symbol] a duplication policy for conflict resolution
163
+ # @param chunk_size [Integer] set default chunk size, in bytes, for the time series
164
+ #
165
+ # @return [Sample] the value that was added
166
+ # @raise [Redis::CommandError] if the value being added is older than the latest timestamp in the series
167
+ #
168
+ # @see TimeSeries::DuplicatePolicy
169
+ def add(value, timestamp = '*', uncompressed: nil, on_duplicate: nil, chunk_size: nil)
170
+ ts = cmd 'TS.ADD',
171
+ key,
172
+ timestamp,
173
+ value,
174
+ ('UNCOMPRESSED' if uncompressed),
175
+ (['CHUNK_SIZE', chunk_size] if chunk_size),
176
+ (DuplicatePolicy.new(on_duplicate).to_a('ON_DUPLICATE') if on_duplicate)
63
177
  Sample.new(ts, value)
64
178
  end
65
179
 
66
- def create(labels: nil)
67
- args = [key]
68
- args << ['RETENTION', retention] if retention
69
- args << 'UNCOMPRESSED' if uncompressed
70
- args << ['LABELS', labels.to_a] if labels&.any?
71
- cmd 'TS.CREATE', args.flatten
180
+ # Issues a TS.CREATE command for the current series.
181
+ # You should use class method {Redis::TimeSeries.create} instead.
182
+ # @api private
183
+ def create(retention: nil, uncompressed: nil, labels: nil, duplicate_policy: nil, chunk_size: nil)
184
+ cmd 'TS.CREATE', key,
185
+ (['RETENTION', retention] if retention),
186
+ ('UNCOMPRESSED' if uncompressed),
187
+ (['CHUNK_SIZE', chunk_size] if chunk_size),
188
+ (DuplicatePolicy.new(duplicate_policy).to_a if duplicate_policy),
189
+ (['LABELS', labels.to_a] if labels&.any?)
72
190
  self
73
191
  end
74
192
 
75
- def decrby(value = 1, timestamp = nil)
76
- args = [key, value]
77
- args << timestamp if timestamp
78
- cmd 'TS.DECRBY', args
193
+ # Create a compaction rule for this series.
194
+ #
195
+ # @param dest [String, TimeSeries] the destination series (or key) to aggregate the data
196
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
197
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
198
+ # aggregation_type and duration +[:avg, 120000]+
199
+ #
200
+ # @return [String] the string "OK"
201
+ # @raise [Redis::TimeSeries::AggregationError] if the given aggregation params are invalid
202
+ # @raise [Redis::CommandError] if the compaction rule cannot be applied to either series
203
+ #
204
+ # @see TimeSeries.create_rule
205
+ def create_rule(dest:, aggregation:)
206
+ self.class.create_rule(source: self, dest: dest, aggregation: aggregation)
207
+ end
208
+
209
+ # Delete an existing compaction rule.
210
+ #
211
+ # @param dest [String, TimeSeries] the destination series (or key) the rule applies to
212
+ #
213
+ # @return [String] the string "OK"
214
+ # @raise [Redis::CommandError] if the compaction rule does not exist
215
+ #
216
+ # @see TimeSeries.delete_rule
217
+ def delete_rule(dest:)
218
+ self.class.delete_rule(source: self, dest: dest)
219
+ end
220
+
221
+ # Decrement the current value of the series.
222
+ #
223
+ # @param value [Integer] the amount to decrement by
224
+ # @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
225
+ # @param uncompressed [Boolean] if true, stores data in an uncompressed format
226
+ # @param chunk_size [Integer] set default chunk size, in bytes, for the time series
227
+ #
228
+ # @return [Integer] the timestamp the value was stored at
229
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
230
+ def decrby(value = 1, timestamp = nil, uncompressed: nil, chunk_size: nil)
231
+ cmd 'TS.DECRBY',
232
+ key,
233
+ value,
234
+ (timestamp if timestamp),
235
+ ('UNCOMPRESSED' if uncompressed),
236
+ (['CHUNK_SIZE', chunk_size] if chunk_size)
79
237
  end
80
238
  alias decrement decrby
81
239
 
240
+
241
+ # Delete all data and remove this time series from Redis.
242
+ #
243
+ # @return [1] if the series existed
244
+ # @return [0] if the series did not exist
82
245
  def destroy
83
246
  redis.del key
84
247
  end
85
248
 
249
+ # Get the most recent sample for this series.
250
+ #
251
+ # @return [Sample] the most recent sample for this series
252
+ # @return [nil] if there are no samples in the series
253
+ #
254
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsget
86
255
  def get
87
256
  cmd('TS.GET', key).then do |timestamp, value|
88
257
  return unless value
@@ -90,28 +259,52 @@ class Redis
90
259
  end
91
260
  end
92
261
 
93
- def incrby(value = 1, timestamp = nil)
94
- args = [key, value]
95
- args << timestamp if timestamp
96
- cmd 'TS.INCRBY', args
262
+ # Increment the current value of the series.
263
+ #
264
+ # @param value [Integer] the amount to increment by
265
+ # @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
266
+ # @param uncompressed [Boolean] if true, stores data in an uncompressed format
267
+ # @param chunk_size [Integer] set default chunk size, in bytes, for the time series
268
+ #
269
+ # @return [Integer] the timestamp the value was stored at
270
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
271
+ def incrby(value = 1, timestamp = nil, uncompressed: nil, chunk_size: nil)
272
+ cmd 'TS.INCRBY',
273
+ key,
274
+ value,
275
+ (timestamp if timestamp),
276
+ ('UNCOMPRESSED' if uncompressed),
277
+ (['CHUNK_SIZE', chunk_size] if chunk_size)
97
278
  end
98
279
  alias increment incrby
99
280
 
281
+ # Get information about the series.
282
+ # Note that all properties of {Info} are also available on the series itself
283
+ # via delegation.
284
+ #
285
+ # @return [Info] an info object about the current series
286
+ #
287
+ # @see Info
288
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsinfo
100
289
  def info
101
- cmd('TS.INFO', key).then(&Info.method(:parse))
290
+ Info.parse series: self, data: cmd('TS.INFO', key)
102
291
  end
103
- def_delegators :info, *Info.members
104
- %i[count length size].each { |m| def_delegator :info, :total_samples, m }
292
+ def_delegators :info, *Info.members - [:series] + %i[count length size source]
105
293
 
294
+ # Assign labels to the series using +TS.ALTER+
295
+ #
296
+ # @param val [Hash] a hash of label-value pairs
297
+ # @return [Hash] the assigned labels
298
+ #
299
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsalter
106
300
  def labels=(val)
107
- cmd 'TS.ALTER', key, 'LABELS', val.to_a.flatten
301
+ cmd 'TS.ALTER', key, 'LABELS', val.to_a
108
302
  end
109
303
 
110
304
  def madd(*values)
111
305
  if values.one? && values.first.is_a?(Hash)
112
306
  # Hash of timestamp => value pairs
113
307
  args = values.first.map do |ts, val|
114
- ts = ts.ts_msec if ts.is_a? Time
115
308
  [key, ts, val]
116
309
  end.flatten
117
310
  elsif values.one? && values.first.is_a?(Array)
@@ -131,30 +324,50 @@ class Redis
131
324
  cmd 'TS.MADD', args
132
325
  end
133
326
 
134
- def range(range, count: nil, agg: nil)
135
- if range.is_a? Hash
136
- args = range.fetch(:from), range.fetch(:to)
137
- elsif range.is_a? Range
138
- args = range.min, range.max
139
- end
140
- args.map! { |ts| (ts.to_f * 1000).to_i }
141
- args.append('COUNT', count) if count
142
- args.append('AGGREGATION', agg) if agg
143
- cmd('TS.RANGE', key, args).map do |ts, val|
144
- Sample.new(ts, val)
327
+ # Get a range of values from the series
328
+ #
329
+ # @param range [Hash, Range] a time range, or hash of +from+ and +to+ values
330
+ # @param count [Integer] the maximum number of results to return
331
+ # @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
332
+ # The aggregation to apply. Can be an {Aggregation} object, or an array of
333
+ # aggregation_type and duration +[:avg, 120000]+
334
+ #
335
+ # @return [Array<Sample>] an array of samples matching the range query
336
+ #
337
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
338
+ 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]
145
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) }
146
353
  end
147
354
 
355
+ # Set data retention time for the series using +TS.ALTER+
356
+ #
357
+ # @param val [Integer] the number of milliseconds data should be retained. +0+ means retain forever.
358
+ # @return [Integer] the retention value of the series
359
+ #
360
+ # @see https://oss.redislabs.com/redistimeseries/commands/#tsalter
148
361
  def retention=(val)
149
- @retention = val.to_i
362
+ # TODO: this should also accept an ActiveSupport::Duration
150
363
  cmd 'TS.ALTER', key, 'RETENTION', val.to_i
151
364
  end
152
365
 
153
- private
154
-
155
- def cmd(name, *args)
156
- puts "DEBUG: #{name} #{args.join(' ')}" if ENV['DEBUG']
157
- redis.call name, *args
366
+ # Compare series based on Redis key and configured client.
367
+ # @return [Boolean] whether the two TimeSeries objects refer to the same series
368
+ def ==(other)
369
+ return false unless other.is_a?(self.class)
370
+ key == other.key && redis == other.redis
158
371
  end
159
372
  end
160
373
  end