pulse-meter 0.0.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.
- data/.gitignore +19 -0
- data/.rbenv-version +1 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/Procfile +3 -0
- data/README.md +440 -0
- data/Rakefile +53 -0
- data/bin/pulse +6 -0
- data/examples/basic.ru +109 -0
- data/examples/basic_sensor_data.rb +38 -0
- data/examples/full/Procfile +2 -0
- data/examples/full/client.rb +82 -0
- data/examples/full/server.ru +114 -0
- data/examples/minimal/Procfile +2 -0
- data/examples/minimal/client.rb +16 -0
- data/examples/minimal/server.ru +20 -0
- data/examples/readme_client_example.rb +52 -0
- data/lib/cmd.rb +150 -0
- data/lib/pulse-meter.rb +17 -0
- data/lib/pulse-meter/mixins/dumper.rb +72 -0
- data/lib/pulse-meter/mixins/utils.rb +91 -0
- data/lib/pulse-meter/sensor.rb +44 -0
- data/lib/pulse-meter/sensor/base.rb +75 -0
- data/lib/pulse-meter/sensor/counter.rb +36 -0
- data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
- data/lib/pulse-meter/sensor/indicator.rb +33 -0
- data/lib/pulse-meter/sensor/timeline.rb +180 -0
- data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
- data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
- data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
- data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
- data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
- data/lib/pulse-meter/version.rb +3 -0
- data/lib/pulse-meter/visualize/app.rb +43 -0
- data/lib/pulse-meter/visualize/dsl.rb +0 -0
- data/lib/pulse-meter/visualize/dsl/errors.rb +46 -0
- data/lib/pulse-meter/visualize/dsl/layout.rb +55 -0
- data/lib/pulse-meter/visualize/dsl/page.rb +50 -0
- data/lib/pulse-meter/visualize/dsl/sensor.rb +21 -0
- data/lib/pulse-meter/visualize/dsl/widget.rb +84 -0
- data/lib/pulse-meter/visualize/layout.rb +54 -0
- data/lib/pulse-meter/visualize/page.rb +30 -0
- data/lib/pulse-meter/visualize/public/css/application.css +19 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.css +4883 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.min.css +729 -0
- data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings-white.png +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings.png +0 -0
- data/lib/pulse-meter/visualize/public/js/application.coffee +262 -0
- data/lib/pulse-meter/visualize/public/js/application.js +279 -0
- data/lib/pulse-meter/visualize/public/js/backbone-min.js +38 -0
- data/lib/pulse-meter/visualize/public/js/bootstrap.js +1835 -0
- data/lib/pulse-meter/visualize/public/js/highcharts.js +203 -0
- data/lib/pulse-meter/visualize/public/js/jquery-1.7.2.min.js +4 -0
- data/lib/pulse-meter/visualize/public/js/json2.js +487 -0
- data/lib/pulse-meter/visualize/public/js/underscore-min.js +32 -0
- data/lib/pulse-meter/visualize/sensor.rb +60 -0
- data/lib/pulse-meter/visualize/views/main.haml +40 -0
- data/lib/pulse-meter/visualize/widget.rb +68 -0
- data/lib/pulse-meter/visualizer.rb +30 -0
- data/lib/test_helpers/matchers.rb +36 -0
- data/pulse-meter.gemspec +39 -0
- data/spec/pulse_meter/mixins/dumper_spec.rb +158 -0
- data/spec/pulse_meter/mixins/utils_spec.rb +134 -0
- data/spec/pulse_meter/sensor/base_spec.rb +97 -0
- data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
- data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
- data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
- data/spec/pulse_meter/sensor/timeline_spec.rb +58 -0
- data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
- data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
- data/spec/pulse_meter/visualize/app_spec.rb +27 -0
- data/spec/pulse_meter/visualize/dsl/layout_spec.rb +64 -0
- data/spec/pulse_meter/visualize/dsl/page_spec.rb +75 -0
- data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +30 -0
- data/spec/pulse_meter/visualize/dsl/widget_spec.rb +127 -0
- data/spec/pulse_meter/visualize/layout_spec.rb +55 -0
- data/spec/pulse_meter/visualize/page_spec.rb +150 -0
- data/spec/pulse_meter/visualize/sensor_spec.rb +120 -0
- data/spec/pulse_meter/visualize/widget_spec.rb +113 -0
- data/spec/pulse_meter/visualizer_spec.rb +42 -0
- data/spec/pulse_meter_spec.rb +16 -0
- data/spec/shared_examples/timeline_sensor.rb +279 -0
- data/spec/shared_examples/timelined_subclass.rb +23 -0
- data/spec/spec_helper.rb +29 -0
- metadata +435 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# Static counter
|
2
|
+
module PulseMeter
|
3
|
+
module Sensor
|
4
|
+
class Counter < Base
|
5
|
+
|
6
|
+
# Cleans up all sensor metadata in Redis
|
7
|
+
def cleanup
|
8
|
+
redis.del(value_key)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
# Increments counter value by 1
|
13
|
+
def incr
|
14
|
+
event(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Processes event by incremnting counter by given value
|
18
|
+
# @param value [Fixnum] increment
|
19
|
+
def event(value)
|
20
|
+
redis.incrby(value_key, value.to_i)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Gets counter value
|
24
|
+
# @return [Fixnum]
|
25
|
+
def value
|
26
|
+
redis.get(value_key).to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
# Gets redis key by which counter value is stored
|
30
|
+
# @return [String]
|
31
|
+
def value_key
|
32
|
+
@value_key ||= "pulse_meter:value:#{name}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# Static hashed counter to count values by multiple keys
|
4
|
+
module PulseMeter
|
5
|
+
module Sensor
|
6
|
+
class HashedCounter < Counter
|
7
|
+
|
8
|
+
# Increments counter value by 1 for given key
|
9
|
+
# @param key [String] key to be incremented
|
10
|
+
def incr(key)
|
11
|
+
event({key => 1})
|
12
|
+
end
|
13
|
+
|
14
|
+
# Processes events for multiple keys
|
15
|
+
# @param data [Hash] hash where keys represent counter keys
|
16
|
+
# and values are increments for their keys
|
17
|
+
def event(data)
|
18
|
+
data.each_pair {|k, v| redis.hincrby(value_key, k, v.to_i)}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returs data stored in counter
|
22
|
+
# @return [Hash]
|
23
|
+
def value
|
24
|
+
redis.
|
25
|
+
hgetall(value_key).
|
26
|
+
inject(Hash.new(0)) {|h, (k, v)| h[k] = v.to_i; h}
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
# Static indicator. In fact is is just a named variable with float value
|
4
|
+
class Indicator < Base
|
5
|
+
|
6
|
+
# Cleans up all sensor metadata in Redis
|
7
|
+
def cleanup
|
8
|
+
redis.del(value_key)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
# Sets indicator value
|
13
|
+
# @param value [Float] new indicator value
|
14
|
+
def event(value)
|
15
|
+
redis.set(value_key, value.to_f)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get indicator value
|
19
|
+
# @return [Fixnum] indicator value or zero unless it was initialized
|
20
|
+
def value
|
21
|
+
val = redis.get(value_key)
|
22
|
+
val.nil? ? 0 : val.to_f
|
23
|
+
end
|
24
|
+
|
25
|
+
# Gets redis key by which counter value is stored
|
26
|
+
# @return [String]
|
27
|
+
def value_key
|
28
|
+
@value_key ||= "pulse_meter:value:#{name}"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module PulseMeter
|
4
|
+
module Sensor
|
5
|
+
# @abstract Represents timelined sensor: series of values,
|
6
|
+
# one value for each consequent time interval.
|
7
|
+
class Timeline < Base
|
8
|
+
include PulseMeter::Mixins::Utils
|
9
|
+
|
10
|
+
# @!attribute [r] interval
|
11
|
+
# @return [Fixnum] Rotation interval
|
12
|
+
# @!attribute [r] ttl
|
13
|
+
# @return [Fixnum] How long summarized data will be stored before expiration
|
14
|
+
# @!attribute [r] raw_data_ttl
|
15
|
+
# @return [Fixnum] How long unsummarized raw data will be stored before expiration
|
16
|
+
# @!attribute [r] reduce_delay
|
17
|
+
# @return [Fixnum] Delay between end of interval and summarization
|
18
|
+
attr_reader :interval, :ttl, :raw_data_ttl, :reduce_delay
|
19
|
+
|
20
|
+
# Default values for some sensor parameters
|
21
|
+
DEFAULTS = {
|
22
|
+
:raw_data_ttl => 3600,
|
23
|
+
:reduce_delay => 60,
|
24
|
+
}
|
25
|
+
|
26
|
+
# Initializes sensor with given name and parameters
|
27
|
+
# @param name [String] sensor name
|
28
|
+
# @option options [Fixnum] :interval Rotation interval
|
29
|
+
# @option options [Fixnum] :ttl How long summarized data will be stored before expiration
|
30
|
+
# @option options [Fixnum] :raw_data_ttl How long unsummarized raw data will be stored before expiration
|
31
|
+
# @option options [Fixnum] :reduce_delay Delay between end of interval and summarization
|
32
|
+
def initialize(name, options)
|
33
|
+
@interval = assert_positive_integer!(options, :interval)
|
34
|
+
@ttl = assert_positive_integer!(options, :ttl)
|
35
|
+
@raw_data_ttl = assert_positive_integer!(options, :raw_data_ttl, DEFAULTS[:raw_data_ttl])
|
36
|
+
@reduce_delay = assert_positive_integer!(options, :reduce_delay, DEFAULTS[:reduce_delay])
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
# Clean up all sensor metadata and data
|
41
|
+
def cleanup
|
42
|
+
keys = redis.keys(raw_data_key('*')) + redis.keys(data_key('*'))
|
43
|
+
multi do
|
44
|
+
keys.each{|key| redis.del(key)}
|
45
|
+
end
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
# Processes event
|
50
|
+
def event(value = nil)
|
51
|
+
multi do
|
52
|
+
current_key = current_raw_data_key
|
53
|
+
aggregate_event(current_key, value)
|
54
|
+
redis.expire(current_key, raw_data_ttl)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Reduces data in given interval.
|
60
|
+
# @note Interval id is
|
61
|
+
# just unixtime of its lower bound. Ruduction is a process
|
62
|
+
# of 'compressing' all interval's raw data to a single value.
|
63
|
+
# When reduction is done summarized data is saved to Redis
|
64
|
+
# separately with expiration time taken from sensor configuration.
|
65
|
+
# @param interval_id [Fixnum]
|
66
|
+
def reduce(interval_id)
|
67
|
+
interval_raw_data_key = raw_data_key(interval_id)
|
68
|
+
return unless redis.exists(interval_raw_data_key)
|
69
|
+
value = summarize(interval_raw_data_key)
|
70
|
+
interval_data_key = data_key(interval_id)
|
71
|
+
multi do
|
72
|
+
redis.del(interval_raw_data_key)
|
73
|
+
redis.set(interval_data_key, value)
|
74
|
+
redis.expire(interval_data_key, ttl)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Reduces data in all raw interval
|
79
|
+
def reduce_all_raw
|
80
|
+
min_time = Time.now - reduce_delay - interval
|
81
|
+
redis.keys(raw_data_key('*')).each do |key|
|
82
|
+
interval_id = key.split(':').last
|
83
|
+
next if Time.at(interval_id.to_i) > min_time
|
84
|
+
reduce(interval_id)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.reduce_all_raw
|
89
|
+
list_objects.each do |sensor|
|
90
|
+
sensor.reduce_all_raw if sensor.respond_to? :reduce_all_raw
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returts sensor data within some last seconds
|
95
|
+
# @param time_ago [Fixnum] interval length in seconds
|
96
|
+
# @return [Array<SensorData>]
|
97
|
+
# @raise ArgumentError if argumets are not valid time objects
|
98
|
+
def timeline(time_ago)
|
99
|
+
raise ArgumentError unless time_ago.respond_to?(:to_i) && time_ago.to_i > 0
|
100
|
+
now = Time.now
|
101
|
+
timeline_within(now - time_ago.to_i, now)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Returts sensor data within given time
|
105
|
+
# @param from [Time] lower bound
|
106
|
+
# @param till [Time] upper bound
|
107
|
+
# @return [Array<SensorData>]
|
108
|
+
# @raise ArgumentError if argumets are not valid time objects
|
109
|
+
def timeline_within(from, till)
|
110
|
+
raise ArgumentError unless from.kind_of?(Time) && till.kind_of?(Time)
|
111
|
+
start_time, end_time = from.to_i, till.to_i
|
112
|
+
current_interval_id = get_interval_id(start_time) + interval
|
113
|
+
res = []
|
114
|
+
while current_interval_id < end_time
|
115
|
+
res << get_timeline_value(current_interval_id)
|
116
|
+
current_interval_id += interval
|
117
|
+
end
|
118
|
+
res
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns sensor data for given interval.
|
122
|
+
# If the interval is not over yet makes its data in-memory summarization
|
123
|
+
# and returns calculated value
|
124
|
+
# @param interval_id [Fixnum]
|
125
|
+
# @return [SensorData]
|
126
|
+
def get_timeline_value(interval_id)
|
127
|
+
interval_data_key = data_key(interval_id)
|
128
|
+
return SensorData.new(Time.at(interval_id), redis.get(interval_data_key)) if redis.exists(interval_data_key)
|
129
|
+
interval_raw_data_key = raw_data_key(interval_id)
|
130
|
+
return SensorData.new(Time.at(interval_id), summarize(interval_raw_data_key)) if redis.exists(interval_raw_data_key)
|
131
|
+
SensorData.new(Time.at(interval_id), nil)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns Redis key by which raw data for current interval is stored
|
135
|
+
def current_raw_data_key
|
136
|
+
raw_data_key(current_interval_id)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns Redis key by which raw data for given interval is stored
|
140
|
+
# @param id [Fixnum] interval id
|
141
|
+
def raw_data_key(id)
|
142
|
+
"pulse_meter:raw:#{name}:#{id}"
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns Redis key by which summarized data for given interval is stored
|
146
|
+
# @param id [Fixnum] interval id
|
147
|
+
def data_key(id)
|
148
|
+
"pulse_meter:data:#{name}:#{id}"
|
149
|
+
end
|
150
|
+
|
151
|
+
# Returns interval id where given time is
|
152
|
+
# @param time [Time]
|
153
|
+
def get_interval_id(time)
|
154
|
+
(time.to_i / interval) * interval
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns current interval id
|
158
|
+
# @return [Fixnum]
|
159
|
+
def current_interval_id
|
160
|
+
get_interval_id(Time.now)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @abstract Registeres event for current interval identified by key
|
164
|
+
# @param key [Fixnum] interval id
|
165
|
+
# @param value [Object] value to be aggregated
|
166
|
+
def aggregate_event(key, value)
|
167
|
+
# simple
|
168
|
+
redis.set(key, value)
|
169
|
+
end
|
170
|
+
|
171
|
+
# @abstract Summarizes all event within interval to a single value
|
172
|
+
# @param key [Fixnum] interval_id
|
173
|
+
def summarize(key)
|
174
|
+
# simple
|
175
|
+
redis.get(key)
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
module Timelined
|
4
|
+
# Average value over interval
|
5
|
+
class Average < Timeline
|
6
|
+
|
7
|
+
def aggregate_event(key, value)
|
8
|
+
redis.hincrby(key, :count, 1)
|
9
|
+
redis.hincrby(key, :sum, value)
|
10
|
+
end
|
11
|
+
|
12
|
+
def summarize(key)
|
13
|
+
count = redis.hget(key, :count)
|
14
|
+
sum = redis.hget(key, :sum)
|
15
|
+
if count && !count.empty?
|
16
|
+
sum.to_f / count.to_f
|
17
|
+
else
|
18
|
+
0
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
module Timelined
|
4
|
+
# Counts events per interval
|
5
|
+
class Counter < Timeline
|
6
|
+
def aggregate_event(key, value)
|
7
|
+
redis.incrby(key, value.to_i)
|
8
|
+
end
|
9
|
+
|
10
|
+
def summarize(key)
|
11
|
+
redis.get(key).to_i
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module PulseMeter
|
4
|
+
module Sensor
|
5
|
+
module Timelined
|
6
|
+
# Counts multiple types of events per interval.
|
7
|
+
# Good replacement for multiple counters to be visualized together
|
8
|
+
class HashedCounter < Timeline
|
9
|
+
def aggregate_event(key, data)
|
10
|
+
data.each_pair {|k, v| redis.hincrby(key, k, v)}
|
11
|
+
end
|
12
|
+
|
13
|
+
def summarize(key)
|
14
|
+
redis.
|
15
|
+
hgetall(key).
|
16
|
+
inject({}) {|h, (k, v)| h[k] = v.to_i; h}.
|
17
|
+
to_json
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
module Timelined
|
4
|
+
# Calculates max value in interval
|
5
|
+
class Max < Timeline
|
6
|
+
|
7
|
+
def aggregate_event(key, value)
|
8
|
+
redis.zadd(key, value, "#{value}::#{uniqid}")
|
9
|
+
redis.zremrangebyrank(key, 0, -2)
|
10
|
+
end
|
11
|
+
|
12
|
+
def summarize(key)
|
13
|
+
count = redis.zcard(key)
|
14
|
+
if count > 0
|
15
|
+
max_el = redis.zrange(key, -1, -1)[0]
|
16
|
+
redis.zscore(key, max_el).to_f
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
module Timelined
|
4
|
+
# Calculates min value in interval
|
5
|
+
class Min < Timeline
|
6
|
+
|
7
|
+
def aggregate_event(key, value)
|
8
|
+
redis.zadd(key, value, "#{value}::#{uniqid}")
|
9
|
+
redis.zremrangebyrank(key, 1, -1)
|
10
|
+
end
|
11
|
+
|
12
|
+
def summarize(key)
|
13
|
+
count = redis.zcard(key)
|
14
|
+
if count > 0
|
15
|
+
min_el = redis.zrange(key, 0, 0)[0]
|
16
|
+
redis.zscore(key, min_el).to_f
|
17
|
+
else
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
module Timelined
|
4
|
+
# Calculates n'th percentile in interval
|
5
|
+
class Percentile < Timeline
|
6
|
+
attr_reader :p_value
|
7
|
+
|
8
|
+
def initialize(name, options)
|
9
|
+
@p_value = assert_ranged_float!(options, :p, 0, 1)
|
10
|
+
super(name, options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def aggregate_event(key, value)
|
14
|
+
redis.zadd(key, value, "#{value}::#{uniqid}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def summarize(key)
|
18
|
+
count = redis.zcard(key)
|
19
|
+
if count > 0
|
20
|
+
position = @p_value > 0 ? (@p_value * count).round - 1 : 0
|
21
|
+
el = redis.zrange(key, position, position)[0]
|
22
|
+
redis.zscore(key, el).to_f
|
23
|
+
else
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|