redis-time-series 0.5.2 → 0.7.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +11 -4
- data/.gitignore +1 -0
- data/Appraisals +7 -0
- data/CHANGELOG.md +17 -0
- data/README.md +34 -5
- data/lib/ext/time_msec.rb +8 -0
- data/lib/redis/time_series/duplicate_policy.rb +49 -0
- data/lib/redis/time_series/errors.rb +5 -0
- data/lib/redis/time_series/info.rb +41 -14
- data/lib/redis/time_series/multi.rb +72 -0
- data/lib/redis/time_series/version.rb +1 -1
- data/lib/redis/time_series.rb +163 -46
- data/lib/redis-time-series.rb +3 -0
- data/redis-time-series.gemspec +3 -2
- metadata +30 -14
- data/Gemfile.lock +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16477ce0296ad40319f56ba74622e19816b6739ef15f8ec1b173f560e6966020
|
4
|
+
data.tar.gz: 878d7209216e9bed1b565ed607a4cb882b795efdf879cee33956377b8ddd4928
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 260b8d730198d05ae379e5b97c8135bc06d62b4ad69dc67e37b6af7a65a0c9ff8f13f3d4669e45280b7d7f4b14496d8ef62c35a0f57b5523b91d41bd8b3f46d2
|
7
|
+
data.tar.gz: 3ab2ec330930620a7b20f911d5279e7cdab8c56fcf73472c8daae4d77630c315f355437c819beeae0b82038cf5f8ab5ba14df0234fed76e72a21b2e0b138c1ae
|
data/.github/workflows/rspec.yml
CHANGED
@@ -11,9 +11,14 @@ on:
|
|
11
11
|
jobs:
|
12
12
|
spec:
|
13
13
|
runs-on: ubuntu-latest
|
14
|
+
strategy:
|
15
|
+
fail-fast: false
|
16
|
+
matrix:
|
17
|
+
image_version: ['latest', 'edge']
|
18
|
+
ruby_version: ['2.6', '2.7', '3.0', '3.1']
|
14
19
|
services:
|
15
20
|
redis:
|
16
|
-
image: redislabs/redistimeseries
|
21
|
+
image: redislabs/redistimeseries:${{ matrix.image_version }}
|
17
22
|
ports:
|
18
23
|
- 6379:6379/tcp
|
19
24
|
env:
|
@@ -25,16 +30,18 @@ jobs:
|
|
25
30
|
- name: Set up Ruby
|
26
31
|
uses: ruby/setup-ruby@v1
|
27
32
|
with:
|
28
|
-
ruby-version:
|
33
|
+
ruby-version: ${{ matrix.ruby_version }}
|
29
34
|
- name: Set up CodeClimate
|
30
35
|
run: |
|
31
36
|
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
32
37
|
chmod +x ./cc-test-reporter
|
33
38
|
./cc-test-reporter before-build
|
34
39
|
- name: Install dependencies
|
35
|
-
run:
|
40
|
+
run: |
|
41
|
+
bundle install
|
42
|
+
bundle exec appraisal install
|
36
43
|
- name: Run specs
|
37
|
-
run: bundle exec rake spec
|
44
|
+
run: bundle exec appraisal rake spec
|
38
45
|
- name: Upload coverage report
|
39
46
|
run: ./cc-test-reporter after-build -t simplecov coverage/.resultset.json
|
40
47
|
- uses: actions/upload-artifact@v2
|
data/.gitignore
CHANGED
data/Appraisals
ADDED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,23 @@
|
|
2
2
|
|
3
3
|
## Unreleased
|
4
4
|
|
5
|
+
## 0.7.1
|
6
|
+
* Handle ActiveSupport::TimeWithZone objects (#75)
|
7
|
+
|
8
|
+
## 0.7.0
|
9
|
+
* Add Ruby 3.1 to build matrix (#70)
|
10
|
+
* Add Ruby 3.0 to build matrix (#63)
|
11
|
+
* Relax Redis version constraint (#62)
|
12
|
+
* Add TS.REVRANGE, TS.MRANGE, and TS.MREVRANGE commands (#19)
|
13
|
+
* Update TS.MADD commands to consolidate parsing (#58)
|
14
|
+
|
15
|
+
## 0.6.0
|
16
|
+
* Add CHUNK_SIZE param to CREATE, ADD, INCRBY, DECRBY commands (#53)
|
17
|
+
* Add duplication policy to TS.CREATE and TS.ADD commands (#51)
|
18
|
+
* Add support for endless ranges to TS.RANGE (#50)
|
19
|
+
* Cast label values to integers in Info struct (#49)
|
20
|
+
* Build against edge upstream in addition to latest stable (#48)
|
21
|
+
|
5
22
|
## 0.5.2
|
6
23
|
* Add chunk_type to info struct (#47)
|
7
24
|
|
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
|
-
*
|
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
|
|
data/lib/ext/time_msec.rb
CHANGED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class Redis
|
3
|
+
class TimeSeries
|
4
|
+
# Duplication policies can be applied to a time series in order to resolve conflicts
|
5
|
+
# when adding data that already exists in the series.
|
6
|
+
#
|
7
|
+
# @see https://oss.redislabs.com/redistimeseries/master/configuration/#duplicate_policy
|
8
|
+
class DuplicatePolicy
|
9
|
+
VALID_POLICIES = %i[
|
10
|
+
block
|
11
|
+
first
|
12
|
+
last
|
13
|
+
min
|
14
|
+
max
|
15
|
+
sum
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
attr_reader :policy
|
19
|
+
|
20
|
+
def initialize(policy)
|
21
|
+
policy = policy.to_s.downcase.to_sym
|
22
|
+
if VALID_POLICIES.include?(policy)
|
23
|
+
@policy = policy
|
24
|
+
else
|
25
|
+
raise UnknownPolicyError, "#{policy} is not a valid duplicate policy"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_a(cmd = 'DUPLICATE_POLICY')
|
30
|
+
[cmd, policy]
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s(cmd = 'DUPLICATE_POLICY')
|
34
|
+
to_a(cmd).join(' ')
|
35
|
+
end
|
36
|
+
|
37
|
+
def ==(other)
|
38
|
+
return policy == other.policy if other.is_a?(self.class)
|
39
|
+
policy == self.class.new(other).policy
|
40
|
+
end
|
41
|
+
|
42
|
+
VALID_POLICIES.each do |policy|
|
43
|
+
define_method("#{policy}?") do
|
44
|
+
@policy == policy
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -15,5 +15,10 @@ class Redis
|
|
15
15
|
# an unknown type, or when calling a command with an invalid aggregation value.
|
16
16
|
# @see Redis::TimeSeries::Aggregation
|
17
17
|
class AggregationError < Error; end
|
18
|
+
|
19
|
+
# +UnknownPolicyError+ is raised when attempting to apply an unkown type of
|
20
|
+
# duplicate policy when creating or adding to a series.
|
21
|
+
# @see Redis::TimeSeries::DuplicatePolicy
|
22
|
+
class UnknownPolicyError < Error; end
|
18
23
|
end
|
19
24
|
end
|
@@ -10,6 +10,10 @@ class Redis
|
|
10
10
|
#
|
11
11
|
# @!attribute [r] chunk_count
|
12
12
|
# @return [Integer] number of memory chunks used for the time-series
|
13
|
+
# @!attribute [r] chunk_size
|
14
|
+
# @return [Integer] amount of allocated memory in bytes
|
15
|
+
# @!attribute [r] chunk_type
|
16
|
+
# @return [String] whether the chunk is "compressed" or "uncompressed"
|
13
17
|
# @!attribute [r] first_timestamp
|
14
18
|
# @return [Integer] first timestamp present in the time-series (milliseconds since epoch)
|
15
19
|
# @!attribute [r] labels
|
@@ -52,20 +56,43 @@ class Redis
|
|
52
56
|
:total_samples,
|
53
57
|
keyword_init: true
|
54
58
|
) do
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
59
|
+
class << self
|
60
|
+
# @api private
|
61
|
+
# @return [Info]
|
62
|
+
def parse(series:, data:)
|
63
|
+
build_hash(data)
|
64
|
+
.merge(series: series)
|
65
|
+
.then(&method(:parse_labels))
|
66
|
+
.then(&method(:parse_policies))
|
67
|
+
.then(&method(:parse_rules))
|
68
|
+
.then(&method(:new))
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def build_hash(data)
|
74
|
+
data.each_slice(2).reduce({}) do |h, (key, value)|
|
75
|
+
# Convert camelCase info keys to snake_case
|
76
|
+
key = key.gsub(/(.)([A-Z])/,'\1_\2').downcase.to_sym
|
77
|
+
# Skip unknown properties
|
78
|
+
next h unless members.include?(key)
|
79
|
+
h.merge(key => value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_labels(hash)
|
84
|
+
hash[:labels] = hash[:labels].to_h.transform_values { |v| v.to_i.to_s == v ? v.to_i : v }
|
85
|
+
hash
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_policies(hash)
|
89
|
+
hash[:duplicate_policy] = DuplicatePolicy.new(hash[:duplicate_policy]) if hash[:duplicate_policy]
|
90
|
+
hash
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_rules(hash)
|
94
|
+
hash[:rules] = hash[:rules].map { |d| Rule.new(source: hash[:series], data: d) }
|
95
|
+
hash
|
69
96
|
end
|
70
97
|
end
|
71
98
|
|
@@ -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
|
data/lib/redis/time_series.rb
CHANGED
@@ -37,6 +37,12 @@ class Redis
|
|
37
37
|
# With no value, the series will not be trimmed.
|
38
38
|
# @option options [Boolean] :uncompressed
|
39
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. Must be a multiple
|
45
|
+
# of 8. Default for a series is 4096.
|
40
46
|
#
|
41
47
|
# @return [Redis::TimeSeries] the created time series
|
42
48
|
# @see https://oss.redislabs.com/redistimeseries/commands/#tscreate
|
@@ -83,6 +89,23 @@ class Redis
|
|
83
89
|
redis.del key
|
84
90
|
end
|
85
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
|
+
#
|
86
109
|
def madd(data)
|
87
110
|
data.reduce([]) do |memo, (key, value)|
|
88
111
|
memo << parse_madd_values(key, value)
|
@@ -93,6 +116,46 @@ class Redis
|
|
93
116
|
end
|
94
117
|
end
|
95
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
|
96
159
|
|
97
160
|
# Search for a time series matching the provided filters. Refer to the {Filters} documentation
|
98
161
|
# for more details on how to filter.
|
@@ -119,19 +182,36 @@ class Redis
|
|
119
182
|
|
120
183
|
private
|
121
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
|
+
|
122
199
|
def key_for(series_or_string)
|
123
200
|
series_or_string.is_a?(self) ? series_or_string.key : series_or_string.to_s
|
124
201
|
end
|
125
202
|
|
126
203
|
def parse_madd_values(key, raw)
|
127
|
-
if raw.is_a?
|
204
|
+
if raw.is_a? Hash
|
128
205
|
# multiple timestamp => value pairs
|
129
206
|
raw.map do |timestamp, value|
|
130
207
|
[key, timestamp, value]
|
131
208
|
end
|
132
209
|
elsif raw.is_a? Array
|
133
|
-
#
|
134
|
-
|
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
|
135
215
|
else
|
136
216
|
# single value, no timestamp
|
137
217
|
[key, '*', raw]
|
@@ -154,21 +234,33 @@ class Redis
|
|
154
234
|
# @param value [Numeric] the value to add
|
155
235
|
# @param timestamp [Time, Numeric] the +Time+, or integer timestamp in milliseconds, to add the value
|
156
236
|
# @param uncompressed [Boolean] if true, stores data in an uncompressed format
|
237
|
+
# @param on_duplicate [String, Symbol] a duplication policy for conflict resolution
|
238
|
+
# @param chunk_size [Integer] set default chunk size, in bytes, for the time series
|
157
239
|
#
|
158
240
|
# @return [Sample] the value that was added
|
159
241
|
# @raise [Redis::CommandError] if the value being added is older than the latest timestamp in the series
|
160
|
-
|
161
|
-
|
242
|
+
#
|
243
|
+
# @see TimeSeries::DuplicatePolicy
|
244
|
+
def add(value, timestamp = '*', uncompressed: nil, on_duplicate: nil, chunk_size: nil)
|
245
|
+
ts = cmd 'TS.ADD',
|
246
|
+
key,
|
247
|
+
timestamp,
|
248
|
+
value,
|
249
|
+
('UNCOMPRESSED' if uncompressed),
|
250
|
+
(['CHUNK_SIZE', chunk_size] if chunk_size),
|
251
|
+
(DuplicatePolicy.new(on_duplicate).to_a('ON_DUPLICATE') if on_duplicate)
|
162
252
|
Sample.new(ts, value)
|
163
253
|
end
|
164
254
|
|
165
255
|
# Issues a TS.CREATE command for the current series.
|
166
256
|
# You should use class method {Redis::TimeSeries.create} instead.
|
167
257
|
# @api private
|
168
|
-
def create(retention: nil, uncompressed: nil, labels: nil)
|
258
|
+
def create(retention: nil, uncompressed: nil, labels: nil, duplicate_policy: nil, chunk_size: nil)
|
169
259
|
cmd 'TS.CREATE', key,
|
170
260
|
(['RETENTION', retention] if retention),
|
171
261
|
('UNCOMPRESSED' if uncompressed),
|
262
|
+
(['CHUNK_SIZE', chunk_size] if chunk_size),
|
263
|
+
(DuplicatePolicy.new(duplicate_policy).to_a if duplicate_policy),
|
172
264
|
(['LABELS', labels.to_a] if labels&.any?)
|
173
265
|
self
|
174
266
|
end
|
@@ -206,11 +298,17 @@ class Redis
|
|
206
298
|
# @param value [Integer] the amount to decrement by
|
207
299
|
# @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
|
208
300
|
# @param uncompressed [Boolean] if true, stores data in an uncompressed format
|
301
|
+
# @param chunk_size [Integer] set default chunk size, in bytes, for the time series
|
209
302
|
#
|
210
303
|
# @return [Integer] the timestamp the value was stored at
|
211
304
|
# @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
|
212
|
-
def decrby(value = 1, timestamp = nil, uncompressed: nil)
|
213
|
-
cmd 'TS.DECRBY',
|
305
|
+
def decrby(value = 1, timestamp = nil, uncompressed: nil, chunk_size: nil)
|
306
|
+
cmd 'TS.DECRBY',
|
307
|
+
key,
|
308
|
+
value,
|
309
|
+
(timestamp if timestamp),
|
310
|
+
('UNCOMPRESSED' if uncompressed),
|
311
|
+
(['CHUNK_SIZE', chunk_size] if chunk_size)
|
214
312
|
end
|
215
313
|
alias decrement decrby
|
216
314
|
|
@@ -241,11 +339,17 @@ class Redis
|
|
241
339
|
# @param value [Integer] the amount to increment by
|
242
340
|
# @param timestamp [Time, Integer] the Time or integer millisecond timestamp to save the new value at
|
243
341
|
# @param uncompressed [Boolean] if true, stores data in an uncompressed format
|
342
|
+
# @param chunk_size [Integer] set default chunk size, in bytes, for the time series
|
244
343
|
#
|
245
344
|
# @return [Integer] the timestamp the value was stored at
|
246
345
|
# @see https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby
|
247
|
-
def incrby(value = 1, timestamp = nil, uncompressed: nil)
|
248
|
-
cmd 'TS.INCRBY',
|
346
|
+
def incrby(value = 1, timestamp = nil, uncompressed: nil, chunk_size: nil)
|
347
|
+
cmd 'TS.INCRBY',
|
348
|
+
key,
|
349
|
+
value,
|
350
|
+
(timestamp if timestamp),
|
351
|
+
('UNCOMPRESSED' if uncompressed),
|
352
|
+
(['CHUNK_SIZE', chunk_size] if chunk_size)
|
249
353
|
end
|
250
354
|
alias increment incrby
|
251
355
|
|
@@ -272,32 +376,30 @@ class Redis
|
|
272
376
|
cmd 'TS.ALTER', key, 'LABELS', val.to_a
|
273
377
|
end
|
274
378
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
args = values.each_with_index.map do |val, idx|
|
291
|
-
[key, initial_ts + idx, val]
|
292
|
-
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])
|
293
394
|
end
|
294
|
-
# TODO: return Sample objects here
|
295
|
-
cmd 'TS.MADD', args
|
296
395
|
end
|
396
|
+
alias multi_add madd
|
397
|
+
alias add_multiple madd
|
297
398
|
|
298
|
-
# Get a range of values from the series
|
399
|
+
# Get a range of values from the series, from earliest to most recent
|
299
400
|
#
|
300
|
-
# @param range [
|
401
|
+
# @param range [Range] A time range over which to query. Beginless and endless ranges
|
402
|
+
# indicate oldest and most recent timestamp, respectively.
|
301
403
|
# @param count [Integer] the maximum number of results to return
|
302
404
|
# @param aggregation [Array(<String, Symbol>, Integer), Aggregation]
|
303
405
|
# The aggregation to apply. Can be an {Aggregation} object, or an array of
|
@@ -307,20 +409,23 @@ class Redis
|
|
307
409
|
#
|
308
410
|
# @see https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange
|
309
411
|
def range(range, count: nil, aggregation: nil)
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
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)
|
324
429
|
end
|
325
430
|
|
326
431
|
# Set data retention time for the series using +TS.ALTER+
|
@@ -340,5 +445,17 @@ class Redis
|
|
340
445
|
return false unless other.is_a?(self.class)
|
341
446
|
key == other.key && redis == other.redis
|
342
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
|
343
460
|
end
|
344
461
|
end
|
data/lib/redis-time-series.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
1
|
require 'bigdecimal'
|
2
2
|
require 'forwardable'
|
3
3
|
require 'ext/time_msec'
|
4
|
+
|
4
5
|
require 'redis/time_series/client'
|
5
6
|
require 'redis/time_series/errors'
|
6
7
|
require 'redis/time_series/aggregation'
|
8
|
+
require 'redis/time_series/duplicate_policy'
|
7
9
|
require 'redis/time_series/filters'
|
10
|
+
require 'redis/time_series/multi'
|
8
11
|
require 'redis/time_series/rule'
|
9
12
|
require 'redis/time_series/info'
|
10
13
|
require 'redis/time_series/sample'
|
data/redis-time-series.gemspec
CHANGED
@@ -31,10 +31,11 @@ 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', '
|
34
|
+
spec.add_dependency 'redis', '>= 3.3'
|
35
35
|
|
36
36
|
spec.add_development_dependency 'activesupport', '~> 6.0'
|
37
|
-
spec.add_development_dependency '
|
37
|
+
spec.add_development_dependency 'appraisal', '>= 2.4.1'
|
38
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
38
39
|
spec.add_development_dependency 'pry', '~> 0.13'
|
39
40
|
spec.add_development_dependency 'rake', '~> 13.0'
|
40
41
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-time-series
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Duszynski
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-07-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.3'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '3.3'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,20 +38,34 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '6.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: appraisal
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 2.4.1
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 2.4.1
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
44
58
|
requirements:
|
45
59
|
- - "~>"
|
46
60
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
61
|
+
version: '2.0'
|
48
62
|
type: :development
|
49
63
|
prerelease: false
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
51
65
|
requirements:
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
68
|
+
version: '2.0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: pry
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -108,7 +122,7 @@ dependencies:
|
|
108
122
|
- - "<"
|
109
123
|
- !ruby/object:Gem::Version
|
110
124
|
version: '0.18'
|
111
|
-
description:
|
125
|
+
description:
|
112
126
|
email:
|
113
127
|
- dzunk@hey.com
|
114
128
|
executables: []
|
@@ -118,9 +132,9 @@ files:
|
|
118
132
|
- ".github/workflows/rspec.yml"
|
119
133
|
- ".gitignore"
|
120
134
|
- ".rspec"
|
135
|
+
- Appraisals
|
121
136
|
- CHANGELOG.md
|
122
137
|
- Gemfile
|
123
|
-
- Gemfile.lock
|
124
138
|
- LICENSE.txt
|
125
139
|
- README.md
|
126
140
|
- Rakefile
|
@@ -131,9 +145,11 @@ files:
|
|
131
145
|
- lib/redis/time_series.rb
|
132
146
|
- lib/redis/time_series/aggregation.rb
|
133
147
|
- lib/redis/time_series/client.rb
|
148
|
+
- lib/redis/time_series/duplicate_policy.rb
|
134
149
|
- lib/redis/time_series/errors.rb
|
135
150
|
- lib/redis/time_series/filters.rb
|
136
151
|
- lib/redis/time_series/info.rb
|
152
|
+
- lib/redis/time_series/multi.rb
|
137
153
|
- lib/redis/time_series/rule.rb
|
138
154
|
- lib/redis/time_series/sample.rb
|
139
155
|
- lib/redis/time_series/version.rb
|
@@ -145,7 +161,7 @@ metadata:
|
|
145
161
|
homepage_uri: https://github.com/dzunk/redis-time-series
|
146
162
|
source_code_uri: https://github.com/dzunk/redis-time-series
|
147
163
|
changelog_uri: https://github.com/dzunk/redis-time-series/blob/master/CHANGELOG.md
|
148
|
-
post_install_message:
|
164
|
+
post_install_message:
|
149
165
|
rdoc_options: []
|
150
166
|
require_paths:
|
151
167
|
- lib
|
@@ -160,8 +176,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
160
176
|
- !ruby/object:Gem::Version
|
161
177
|
version: '0'
|
162
178
|
requirements: []
|
163
|
-
rubygems_version: 3.
|
164
|
-
signing_key:
|
179
|
+
rubygems_version: 3.2.22
|
180
|
+
signing_key:
|
165
181
|
specification_version: 4
|
166
182
|
summary: A Ruby adapter for the RedisTimeSeries module.
|
167
183
|
test_files: []
|
data/Gemfile.lock
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
redis-time-series (0.5.2)
|
5
|
-
redis (~> 4.0)
|
6
|
-
|
7
|
-
GEM
|
8
|
-
remote: https://rubygems.org/
|
9
|
-
specs:
|
10
|
-
activesupport (6.0.3.1)
|
11
|
-
concurrent-ruby (~> 1.0, >= 1.0.2)
|
12
|
-
i18n (>= 0.7, < 2)
|
13
|
-
minitest (~> 5.1)
|
14
|
-
tzinfo (~> 1.1)
|
15
|
-
zeitwerk (~> 2.2, >= 2.2.2)
|
16
|
-
coderay (1.1.3)
|
17
|
-
concurrent-ruby (1.1.6)
|
18
|
-
diff-lcs (1.3)
|
19
|
-
docile (1.3.2)
|
20
|
-
i18n (1.8.3)
|
21
|
-
concurrent-ruby (~> 1.0)
|
22
|
-
json (2.3.1)
|
23
|
-
method_source (1.0.0)
|
24
|
-
minitest (5.14.1)
|
25
|
-
pry (0.13.1)
|
26
|
-
coderay (~> 1.1)
|
27
|
-
method_source (~> 1.0)
|
28
|
-
rake (13.0.1)
|
29
|
-
redis (4.2.2)
|
30
|
-
rspec (3.9.0)
|
31
|
-
rspec-core (~> 3.9.0)
|
32
|
-
rspec-expectations (~> 3.9.0)
|
33
|
-
rspec-mocks (~> 3.9.0)
|
34
|
-
rspec-core (3.9.2)
|
35
|
-
rspec-support (~> 3.9.3)
|
36
|
-
rspec-expectations (3.9.2)
|
37
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
38
|
-
rspec-support (~> 3.9.0)
|
39
|
-
rspec-mocks (3.9.1)
|
40
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
41
|
-
rspec-support (~> 3.9.0)
|
42
|
-
rspec-support (3.9.3)
|
43
|
-
simplecov (0.17.1)
|
44
|
-
docile (~> 1.1)
|
45
|
-
json (>= 1.8, < 3)
|
46
|
-
simplecov-html (~> 0.10.0)
|
47
|
-
simplecov-html (0.10.2)
|
48
|
-
thread_safe (0.3.6)
|
49
|
-
tzinfo (1.2.7)
|
50
|
-
thread_safe (~> 0.1)
|
51
|
-
zeitwerk (2.3.0)
|
52
|
-
|
53
|
-
PLATFORMS
|
54
|
-
ruby
|
55
|
-
|
56
|
-
DEPENDENCIES
|
57
|
-
activesupport (~> 6.0)
|
58
|
-
bundler (~> 1.17)
|
59
|
-
pry (~> 0.13)
|
60
|
-
rake (~> 13.0)
|
61
|
-
redis-time-series!
|
62
|
-
rspec (~> 3.0)
|
63
|
-
simplecov (< 0.18)
|
64
|
-
|
65
|
-
BUNDLED WITH
|
66
|
-
1.17.2
|