trifle-stats 2.4.1 → 2.6.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +21 -2
- data/lib/trifle/stats/configuration.rb +1 -0
- data/lib/trifle/stats/driver/api.rb +131 -0
- data/lib/trifle/stats/driver/mysql.rb +33 -15
- data/lib/trifle/stats/driver/postgres.rb +66 -37
- data/lib/trifle/stats/driver/redis.rb +37 -30
- data/lib/trifle/stats/driver/sqlite.rb +66 -35
- data/lib/trifle/stats/nocturnal.rb +2 -2
- data/lib/trifle/stats/operations/timeseries/increment.rb +10 -0
- data/lib/trifle/stats/operations/timeseries/set.rb +10 -0
- data/lib/trifle/stats/transponder/expression.rb +117 -0
- data/lib/trifle/stats/transponder/expression_engine.rb +356 -0
- data/lib/trifle/stats/version.rb +1 -1
- data/lib/trifle/stats.rb +4 -10
- metadata +5 -13
- data/lib/trifle/stats/nocturnal_legacy.rb +0 -161
- data/lib/trifle/stats/transponder/add.rb +0 -30
- data/lib/trifle/stats/transponder/divide.rb +0 -30
- data/lib/trifle/stats/transponder/max.rb +0 -31
- data/lib/trifle/stats/transponder/mean.rb +0 -31
- data/lib/trifle/stats/transponder/min.rb +0 -31
- data/lib/trifle/stats/transponder/multiply.rb +0 -30
- data/lib/trifle/stats/transponder/ratio.rb +0 -30
- data/lib/trifle/stats/transponder/standard_deviation.rb +0 -34
- data/lib/trifle/stats/transponder/subtract.rb +0 -30
- data/lib/trifle/stats/transponder/sum.rb +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 053aa86693f158ba5a395ab80396f0b3274bff69481ec80dd4335e782d000bfe
|
|
4
|
+
data.tar.gz: 86be7c68e870d4fe38eac8dcae73634d8237a5334c8071c610932b22b57c735d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfc8e1a9b317808a37acc6799d87a99d856fe4e4f4b792ac223d96f4ac500b2e6c77e25a5e99e1861047524984cf7ce2bd691a6d930404384b973a173ed49e99
|
|
7
|
+
data.tar.gz: 383c36c748fa4266b2d741054f1f22a35baff5806ccb5ab169d3a0cf49c9fabad2fae97e64acb86fe2fbd534600acad0fe5ee6edac7a01faea6340c5b1eb1c13
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
trifle-stats (2.
|
|
4
|
+
trifle-stats (2.6.0)
|
|
5
5
|
tzinfo (~> 2.0)
|
|
6
6
|
|
|
7
7
|
GEM
|
|
@@ -11,7 +11,7 @@ GEM
|
|
|
11
11
|
bigdecimal (4.0.1)
|
|
12
12
|
bson (4.12.1)
|
|
13
13
|
byebug (11.1.3)
|
|
14
|
-
concurrent-ruby (1.3.
|
|
14
|
+
concurrent-ruby (1.3.8)
|
|
15
15
|
diff-lcs (1.4.4)
|
|
16
16
|
dotenv (2.7.6)
|
|
17
17
|
mini_portile2 (2.8.9)
|
data/README.md
CHANGED
|
@@ -57,23 +57,42 @@ Trifle::Stats.values(
|
|
|
57
57
|
#=> { at: [Mon, Tue, Wed, ...], values: [{ "count" => 12, "revenue" => 598_80, ... }, ...] }
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
### 5. Process with Series
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
series = Trifle::Stats.series(
|
|
64
|
+
key: 'orders',
|
|
65
|
+
from: 1.week.ago,
|
|
66
|
+
to: Time.now,
|
|
67
|
+
granularity: :day
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
series.transpond.expression(
|
|
71
|
+
paths: ['revenue', 'count'],
|
|
72
|
+
expression: 'a / b',
|
|
73
|
+
response: 'avg_order'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
series.aggregate.sum(path: 'count')
|
|
77
|
+
```
|
|
78
|
+
|
|
60
79
|
## Drivers
|
|
61
80
|
|
|
62
81
|
| Driver | Backend | Best for |
|
|
63
82
|
|--------|---------|----------|
|
|
83
|
+
| **API** | Trifle Cloud Projects | Hosted metrics without your own database |
|
|
64
84
|
| **Postgres** | JSONB upsert | Most production apps |
|
|
65
85
|
| **Redis** | Hash increment | High-throughput counters |
|
|
66
86
|
| **MongoDB** | Document upsert | Document-oriented stacks |
|
|
67
87
|
| **MySQL** | JSON column | MySQL shops |
|
|
68
88
|
| **SQLite** | JSON1 extension | Single-server apps, dev/test |
|
|
69
89
|
| **Process** | In-memory | Testing |
|
|
70
|
-
| **Dummy** | No-op | Disabled analytics |
|
|
71
90
|
|
|
72
91
|
## Features
|
|
73
92
|
|
|
74
93
|
- **Dynamic time granularities.** Use any interval like `1m`, `10m`, `1h`, `6h`, `1d`, `1w`, `1mo`, `1q`, `1y`.
|
|
75
94
|
- **Nested value hierarchies.** Track dimensional breakdowns in a single call.
|
|
76
|
-
- **Series operations.** Aggregators
|
|
95
|
+
- **Series operations.** Aggregators, the expression transponder, and formatters.
|
|
77
96
|
- **Buffered writes.** Queue metrics in-memory before flushing to reduce write load.
|
|
78
97
|
- **Driver flexibility.** Switch backends without changing application code.
|
|
79
98
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
require 'time'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'zlib'
|
|
9
|
+
require_relative '../version'
|
|
10
|
+
|
|
11
|
+
module Trifle
|
|
12
|
+
module Stats
|
|
13
|
+
module Driver
|
|
14
|
+
class Api
|
|
15
|
+
ENDPOINT = URI('https://app.trifle.io/api/v1/metrics')
|
|
16
|
+
TIMEOUT = 10
|
|
17
|
+
ERROR_BODY_LIMIT = 1024
|
|
18
|
+
|
|
19
|
+
class Error < Trifle::Stats::Error
|
|
20
|
+
attr_reader :status, :response_body, :retry_after, :delivery_unknown
|
|
21
|
+
|
|
22
|
+
def initialize(message, status: nil, response_body: nil, retry_after: nil, delivery_unknown: false)
|
|
23
|
+
super(message)
|
|
24
|
+
@status = status
|
|
25
|
+
@response_body = response_body
|
|
26
|
+
@retry_after = retry_after
|
|
27
|
+
@delivery_unknown = delivery_unknown
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class NetHttpTransport
|
|
32
|
+
def call(uri:, request:, timeout:)
|
|
33
|
+
Net::HTTP.start(
|
|
34
|
+
uri.hostname,
|
|
35
|
+
uri.port,
|
|
36
|
+
use_ssl: uri.scheme == 'https',
|
|
37
|
+
open_timeout: timeout,
|
|
38
|
+
read_timeout: timeout
|
|
39
|
+
) { |http| http.request(request) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_reader :token, :project_id
|
|
44
|
+
|
|
45
|
+
def initialize(token:, project_id:, transport: NetHttpTransport.new)
|
|
46
|
+
raise ArgumentError, 'token must not be empty' if token.to_s.strip.empty?
|
|
47
|
+
raise ArgumentError, 'project_id must not be empty' if project_id.to_s.strip.empty?
|
|
48
|
+
|
|
49
|
+
@token = token.to_s
|
|
50
|
+
@project_id = project_id.to_s
|
|
51
|
+
@transport = transport
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def description
|
|
55
|
+
self.class.name
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def bypass_buffer?
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def direct_write(operation:, key:, at:, values:, untracked: false)
|
|
63
|
+
body = gzip(JSON.generate(
|
|
64
|
+
operation: operation.to_s,
|
|
65
|
+
key: key,
|
|
66
|
+
at: at.to_time.iso8601(6),
|
|
67
|
+
values: values,
|
|
68
|
+
untracked: untracked
|
|
69
|
+
))
|
|
70
|
+
request = Net::HTTP::Post.new(ENDPOINT)
|
|
71
|
+
request['Authorization'] = "Bearer #{token}"
|
|
72
|
+
request['X-Trifle-Source-Id'] = project_id
|
|
73
|
+
request['Content-Type'] = 'application/json'
|
|
74
|
+
request['Accept'] = 'application/json'
|
|
75
|
+
request['Content-Encoding'] = 'gzip'
|
|
76
|
+
request['User-Agent'] = "trifle-stats-ruby/#{Trifle::Stats::VERSION}"
|
|
77
|
+
request.body = body
|
|
78
|
+
|
|
79
|
+
response = @transport.call(uri: ENDPOINT, request: request, timeout: TIMEOUT)
|
|
80
|
+
return true if response.code.to_i.between?(200, 299)
|
|
81
|
+
|
|
82
|
+
response_body = response.body.to_s.byteslice(0, ERROR_BODY_LIMIT)
|
|
83
|
+
raise Error.new(
|
|
84
|
+
"Trifle API returned HTTP #{response.code}",
|
|
85
|
+
status: response.code.to_i,
|
|
86
|
+
response_body: response_body,
|
|
87
|
+
retry_after: response['Retry-After']
|
|
88
|
+
)
|
|
89
|
+
rescue Error
|
|
90
|
+
raise
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
raise Error.new("Trifle API request failed: #{e.message}", delivery_unknown: true), cause: e
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def inc(*)
|
|
96
|
+
unsupported!(:track)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def set(*)
|
|
100
|
+
unsupported!(:assert)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def get(*)
|
|
104
|
+
unsupported!(:values)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def ping(*)
|
|
108
|
+
unsupported!(:beam)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def scan(*)
|
|
112
|
+
unsupported!(:scan)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def gzip(value)
|
|
118
|
+
output = StringIO.new
|
|
119
|
+
Zlib::GzipWriter.wrap(output) { |writer| writer.write(value) }
|
|
120
|
+
output.string
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def unsupported!(operation)
|
|
124
|
+
message = "#{operation} must be called through Trifle::Stats; " \
|
|
125
|
+
'API driver does not support direct storage operations'
|
|
126
|
+
raise Error, message
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -95,10 +95,9 @@ module Trifle
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def get(keys:)
|
|
98
|
-
keys.map
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
end
|
|
98
|
+
identifiers = keys.map { |key| identifier_for(key) }
|
|
99
|
+
data = get_all(identifiers: identifiers)
|
|
100
|
+
identifiers.map { |identifier| self.class.unpack(hash: data.fetch(lookup_key_for(identifier), {})) }
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
def ping(key:, values:)
|
|
@@ -157,23 +156,42 @@ module Trifle
|
|
|
157
156
|
execute_prepared(connection, query, args)
|
|
158
157
|
end
|
|
159
158
|
|
|
160
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
161
|
-
def
|
|
162
|
-
|
|
159
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
160
|
+
def get_all(identifiers:)
|
|
161
|
+
columns = identifiers.first.keys
|
|
162
|
+
# Select `at` as a plain string to avoid client time zone casting
|
|
163
|
+
columns_sql = columns.map do |column|
|
|
164
|
+
if column == :at
|
|
165
|
+
"DATE_FORMAT(`at`, '%Y-%m-%d %H:%i:%s.%f') AS at"
|
|
166
|
+
else
|
|
167
|
+
self.class.quote_identifier(column)
|
|
168
|
+
end
|
|
169
|
+
end.join(', ')
|
|
170
|
+
conditions = identifiers.map do |identifier|
|
|
171
|
+
"(#{identifier.keys.map { |column| "#{self.class.quote_identifier(column)} = ?" }.join(' AND ')})"
|
|
172
|
+
end.join(' OR ')
|
|
173
|
+
|
|
163
174
|
query = <<~SQL
|
|
164
|
-
SELECT CAST(`data` AS CHAR) AS data
|
|
175
|
+
SELECT #{columns_sql}, CAST(`data` AS CHAR) AS data
|
|
165
176
|
FROM #{self.class.quote_identifier(table_name)}
|
|
166
177
|
WHERE #{conditions}
|
|
167
|
-
LIMIT 1
|
|
168
178
|
SQL
|
|
169
|
-
|
|
170
|
-
return {} if packed_data.nil? || packed_data.empty?
|
|
179
|
+
args = identifiers.flat_map { |identifier| query_values(identifier) }
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
execute_prepared(client, query, args).each_with_object({}) do |row, o|
|
|
182
|
+
identifier = columns.to_h { |column| [column, row[column.to_s]] }
|
|
183
|
+
o[lookup_key_for(identifier)] = JSON.parse(row['data'])
|
|
184
|
+
rescue JSON::ParserError
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
189
|
+
|
|
190
|
+
def lookup_key_for(identifier)
|
|
191
|
+
identifier.map do |_column, value|
|
|
192
|
+
value.is_a?(Time) || value.is_a?(DateTime) ? format_time_value(value) : value.to_s
|
|
193
|
+
end.join('|')
|
|
175
194
|
end
|
|
176
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
177
195
|
|
|
178
196
|
def inc_query(identifier:, data:)
|
|
179
197
|
upsert_query(
|
|
@@ -61,22 +61,26 @@ module Trifle
|
|
|
61
61
|
client.transaction do |c|
|
|
62
62
|
keys.map do |key|
|
|
63
63
|
identifier = identifier_for(key)
|
|
64
|
-
|
|
64
|
+
query, params = inc_query(identifier: identifier, data: data)
|
|
65
|
+
c.exec_params(query, params)
|
|
65
66
|
track_system_data(c, key, count, tracking_key)
|
|
66
67
|
end
|
|
67
68
|
end
|
|
68
69
|
end
|
|
69
70
|
|
|
70
|
-
def inc_query(identifier:, data:)
|
|
71
|
+
def inc_query(identifier:, data:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
71
72
|
columns = identifier.keys.join(', ')
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
placeholders = identifier.each_with_index.map { |_, i| "$#{i + 1}" }.join(', ')
|
|
74
|
+
expression = data.inject("to_jsonb(#{table_name}.data)") do |o, (k, v)|
|
|
75
|
+
path = escape_string(k.to_s)
|
|
76
|
+
"jsonb_set(#{o}, '{#{path}}', (COALESCE(#{table_name}.data->>'#{path}', '0')::numeric + #{numeric_value(k, v)})::text::jsonb)" # rubocop:disable Layout/LineLength
|
|
77
|
+
end
|
|
74
78
|
|
|
75
|
-
<<-SQL
|
|
76
|
-
INSERT INTO #{table_name} (#{columns}, data) VALUES (#{
|
|
77
|
-
ON CONFLICT (#{
|
|
78
|
-
#{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', (COALESCE(#{table_name}.data->>'#{k}', '0')::int + #{v})::text::jsonb)" }};
|
|
79
|
+
query = <<-SQL
|
|
80
|
+
INSERT INTO #{table_name} (#{columns}, data) VALUES (#{placeholders}, $#{identifier.size + 1})
|
|
81
|
+
ON CONFLICT (#{columns}) DO UPDATE SET data = #{expression};
|
|
79
82
|
SQL
|
|
83
|
+
[query, query_params(identifier) + [data.to_json]]
|
|
80
84
|
end
|
|
81
85
|
|
|
82
86
|
def set(keys:, values:, count: 1, tracking_key: nil)
|
|
@@ -84,7 +88,8 @@ module Trifle
|
|
|
84
88
|
client.transaction do |c|
|
|
85
89
|
keys.map do |key|
|
|
86
90
|
identifier = identifier_for(key)
|
|
87
|
-
|
|
91
|
+
query, params = set_query(identifier: identifier, data: data)
|
|
92
|
+
c.exec_params(query, params)
|
|
88
93
|
track_system_data(c, key, count, tracking_key)
|
|
89
94
|
end
|
|
90
95
|
end
|
|
@@ -94,21 +99,24 @@ module Trifle
|
|
|
94
99
|
return unless @system_tracking
|
|
95
100
|
|
|
96
101
|
system_data = system_data_for(key: key, count: count, tracking_key: tracking_key)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
)
|
|
102
|
+
query, params = inc_query(identifier: system_identifier_for(key: key), data: system_data)
|
|
103
|
+
connection.exec_params(query, params)
|
|
100
104
|
end
|
|
101
105
|
|
|
102
|
-
def set_query(identifier:, data:)
|
|
106
|
+
def set_query(identifier:, data:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
103
107
|
columns = identifier.keys.join(', ')
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
placeholders = identifier.each_with_index.map { |_, i| "$#{i + 1}" }.join(', ')
|
|
109
|
+
params = query_params(identifier) + [data.to_json]
|
|
110
|
+
expression = data.inject("to_jsonb(#{table_name}.data)") do |o, (k, v)|
|
|
111
|
+
params << v.to_json
|
|
112
|
+
"jsonb_set(#{o}, '{#{escape_string(k.to_s)}}', $#{params.size}::jsonb)"
|
|
113
|
+
end
|
|
106
114
|
|
|
107
|
-
<<-SQL
|
|
108
|
-
INSERT INTO #{table_name} (#{columns}, data) VALUES (#{
|
|
109
|
-
ON CONFLICT (#{
|
|
110
|
-
#{data.inject("to_jsonb(#{table_name}.data)") { |o, (k, v)| "jsonb_set(#{o}, '{#{k}}', '#{v.to_json}'::jsonb)" }}
|
|
115
|
+
query = <<-SQL
|
|
116
|
+
INSERT INTO #{table_name} (#{columns}, data) VALUES (#{placeholders}, $#{identifier.size + 1})
|
|
117
|
+
ON CONFLICT (#{columns}) DO UPDATE SET data = #{expression}
|
|
111
118
|
SQL
|
|
119
|
+
[query, params]
|
|
112
120
|
end
|
|
113
121
|
|
|
114
122
|
def get(keys:)
|
|
@@ -118,7 +126,8 @@ module Trifle
|
|
|
118
126
|
end
|
|
119
127
|
|
|
120
128
|
def get_all(identifiers:) # rubocop:disable Metrics/AbcSize
|
|
121
|
-
|
|
129
|
+
query, params = get_query(identifiers: identifiers)
|
|
130
|
+
results = client.exec_params(query, params).to_a
|
|
122
131
|
sample = identifiers.first
|
|
123
132
|
|
|
124
133
|
results.each_with_object(Hash.new({})) do |r, o|
|
|
@@ -130,37 +139,44 @@ module Trifle
|
|
|
130
139
|
end
|
|
131
140
|
end
|
|
132
141
|
|
|
133
|
-
def get_query(identifiers:)
|
|
142
|
+
def get_query(identifiers:) # rubocop:disable Metrics/MethodLength
|
|
143
|
+
params = []
|
|
134
144
|
conditions = identifiers.map do |identifier|
|
|
135
|
-
identifier.map
|
|
145
|
+
identifier.map do |k, v|
|
|
146
|
+
params << query_param(v)
|
|
147
|
+
"#{k} = $#{params.size}"
|
|
148
|
+
end.join(' AND ')
|
|
136
149
|
end.join(' OR ')
|
|
137
150
|
|
|
138
|
-
<<-SQL
|
|
151
|
+
query = <<-SQL
|
|
139
152
|
SELECT * FROM #{table_name} WHERE #{conditions};
|
|
140
153
|
SQL
|
|
154
|
+
[query, params]
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
def ping(key:, values:)
|
|
144
158
|
return [] if @joined_identifier
|
|
145
159
|
|
|
146
160
|
data = self.class.pack(hash: { data: values, at: key.at })
|
|
147
|
-
|
|
161
|
+
query, params = ping_query(key: key.key, at: key.at, data: data)
|
|
148
162
|
client.transaction do |c|
|
|
149
|
-
c.
|
|
163
|
+
c.exec_params(query, params)
|
|
150
164
|
end
|
|
151
165
|
end
|
|
152
166
|
|
|
153
167
|
def ping_query(key:, at:, data:)
|
|
154
|
-
<<-SQL
|
|
155
|
-
INSERT INTO #{ping_table_name} (key, at, data) VALUES (
|
|
156
|
-
ON CONFLICT (key) DO UPDATE SET at =
|
|
168
|
+
query = <<-SQL
|
|
169
|
+
INSERT INTO #{ping_table_name} (key, at, data) VALUES ($1, $2, $3)
|
|
170
|
+
ON CONFLICT (key) DO UPDATE SET at = $2, data = $3::jsonb;
|
|
157
171
|
SQL
|
|
172
|
+
[query, [key.to_s, at.iso8601, data.to_json]]
|
|
158
173
|
end
|
|
159
174
|
|
|
160
175
|
def scan(key:)
|
|
161
176
|
return [] if @joined_identifier
|
|
162
177
|
|
|
163
|
-
|
|
178
|
+
query, params = scan_query(key: key.key)
|
|
179
|
+
result = client.exec_params(query, params).to_a.first
|
|
164
180
|
return [] if result.nil?
|
|
165
181
|
|
|
166
182
|
[Time.parse(result['at']), self.class.unpack(hash: JSON.parse(result['data']))]
|
|
@@ -169,9 +185,10 @@ module Trifle
|
|
|
169
185
|
end
|
|
170
186
|
|
|
171
187
|
def scan_query(key:)
|
|
172
|
-
<<-SQL
|
|
173
|
-
SELECT at, data FROM #{ping_table_name} WHERE key =
|
|
188
|
+
query = <<-SQL
|
|
189
|
+
SELECT at, data FROM #{ping_table_name} WHERE key = $1 ORDER BY at DESC LIMIT 1;
|
|
174
190
|
SQL
|
|
191
|
+
[query, [key.to_s]]
|
|
175
192
|
end
|
|
176
193
|
|
|
177
194
|
def self.normalize_joined_identifier(value)
|
|
@@ -185,19 +202,31 @@ module Trifle
|
|
|
185
202
|
|
|
186
203
|
private
|
|
187
204
|
|
|
188
|
-
def
|
|
205
|
+
def query_params(identifier)
|
|
206
|
+
identifier.values.map { |value| query_param(value) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def query_param(value)
|
|
189
210
|
case value
|
|
190
|
-
when String
|
|
191
|
-
"'#{value}'"
|
|
192
211
|
when Time
|
|
193
|
-
|
|
212
|
+
value.utc.strftime('%Y-%m-%d %H:%M:%S+00')
|
|
194
213
|
when Integer, Float
|
|
195
|
-
value
|
|
214
|
+
value
|
|
196
215
|
else
|
|
197
|
-
|
|
216
|
+
value.to_s
|
|
198
217
|
end
|
|
199
218
|
end
|
|
200
219
|
|
|
220
|
+
def numeric_value(key, value)
|
|
221
|
+
return value.to_s if value.is_a?(Numeric)
|
|
222
|
+
|
|
223
|
+
raise ArgumentError, "increment requires numeric value for key #{key.inspect}"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def escape_string(value)
|
|
227
|
+
value.gsub("'", "''")
|
|
228
|
+
end
|
|
229
|
+
|
|
201
230
|
def build_map_key(data)
|
|
202
231
|
return data[:key] if @joined_identifier == :full
|
|
203
232
|
return "#{data[:key]}::#{data[:at]}" if @joined_identifier == :partial
|
|
@@ -31,47 +31,43 @@ module Trifle
|
|
|
31
31
|
self.class.pack(hash: { count: count, keys: { tracking_key => count } })
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def inc(keys:, values:, count: 1, tracking_key: nil) # rubocop:disable Metrics/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
system_data_for(key: key, count: count, tracking_key: tracking_key).each do |k, c|
|
|
46
|
-
client.hincrby(skey, k, c)
|
|
34
|
+
def inc(keys:, values:, count: 1, tracking_key: nil) # rubocop:disable Metrics/MethodLength
|
|
35
|
+
packed = self.class.pack(hash: values)
|
|
36
|
+
client.pipelined do |pipeline|
|
|
37
|
+
keys.each do |key|
|
|
38
|
+
key.prefix = prefix
|
|
39
|
+
pkey = key.join(separator)
|
|
40
|
+
|
|
41
|
+
packed.each do |k, c|
|
|
42
|
+
pipeline.hincrby(pkey, k, c)
|
|
43
|
+
end
|
|
44
|
+
track_system_data(pipeline, key, count, tracking_key)
|
|
47
45
|
end
|
|
48
46
|
end
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
def set(keys:, values:, count: 1, tracking_key: nil)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
system_data_for(key: key, count: count, tracking_key: tracking_key).each do |k, c|
|
|
61
|
-
client.hincrby(skey, k, c)
|
|
50
|
+
packed = self.class.pack(hash: values)
|
|
51
|
+
client.pipelined do |pipeline|
|
|
52
|
+
keys.each do |key|
|
|
53
|
+
key.prefix = prefix
|
|
54
|
+
pkey = key.join(separator)
|
|
55
|
+
|
|
56
|
+
pipeline.hmset(pkey, *packed)
|
|
57
|
+
track_system_data(pipeline, key, count, tracking_key)
|
|
62
58
|
end
|
|
63
59
|
end
|
|
64
60
|
end
|
|
65
61
|
|
|
66
62
|
def get(keys:)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
hash: client.hgetall(pkey)
|
|
73
|
-
)
|
|
63
|
+
results = client.pipelined do |pipeline|
|
|
64
|
+
keys.each do |key|
|
|
65
|
+
key.prefix = prefix
|
|
66
|
+
pipeline.hgetall(key.join(separator))
|
|
67
|
+
end
|
|
74
68
|
end
|
|
69
|
+
|
|
70
|
+
results.map { |hash| self.class.unpack(hash: hash) }
|
|
75
71
|
end
|
|
76
72
|
|
|
77
73
|
def ping(*)
|
|
@@ -81,6 +77,17 @@ module Trifle
|
|
|
81
77
|
def scan(*)
|
|
82
78
|
[]
|
|
83
79
|
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def track_system_data(pipeline, key, count, tracking_key)
|
|
84
|
+
return unless @system_tracking
|
|
85
|
+
|
|
86
|
+
skey = system_join_for(key: key)
|
|
87
|
+
system_data_for(key: key, count: count, tracking_key: tracking_key).each do |k, c|
|
|
88
|
+
pipeline.hincrby(skey, k, c)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
84
91
|
end
|
|
85
92
|
end
|
|
86
93
|
end
|