redis_counters 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +3 -0
- data/Makefile +14 -0
- data/README.md +320 -0
- data/Rakefile +15 -0
- data/lib/redis_counters.rb +21 -0
- data/lib/redis_counters/base_counter.rb +84 -0
- data/lib/redis_counters/bucket.rb +59 -0
- data/lib/redis_counters/cluster.rb +22 -0
- data/lib/redis_counters/clusterize_and_partitionize.rb +194 -0
- data/lib/redis_counters/hash_counter.rb +70 -0
- data/lib/redis_counters/partition.rb +16 -0
- data/lib/redis_counters/unique_hash_counter.rb +51 -0
- data/lib/redis_counters/unique_values_lists/base.rb +57 -0
- data/lib/redis_counters/unique_values_lists/blocking.rb +167 -0
- data/lib/redis_counters/unique_values_lists/expirable.rb +155 -0
- data/lib/redis_counters/unique_values_lists/non_blocking.rb +91 -0
- data/lib/redis_counters/version.rb +3 -0
- data/redis_counters.gemspec +31 -0
- data/spec/redis_counters/base_spec.rb +29 -0
- data/spec/redis_counters/hash_counter_spec.rb +462 -0
- data/spec/redis_counters/unique_hash_counter_spec.rb +83 -0
- data/spec/redis_counters/unique_values_lists/blocking_spec.rb +94 -0
- data/spec/redis_counters/unique_values_lists/expirable_spec.rb +6 -0
- data/spec/redis_counters/unique_values_lists/non_blicking_spec.rb +6 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/unique_values_lists/common.rb +563 -0
- data/spec/support/unique_values_lists/expirable.rb +162 -0
- data/spec/support/unique_values_lists/set.rb +119 -0
- data/tasks/audit.rake +6 -0
- data/tasks/cane.rake +12 -0
- data/tasks/changelog.rake +7 -0
- data/tasks/coverage.rake +21 -0
- data/tasks/support.rb +24 -0
- metadata +242 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'forwardable'
|
3
|
+
require 'active_support/core_ext/class/attribute'
|
4
|
+
|
5
|
+
module RedisCounters
|
6
|
+
|
7
|
+
# Базовый класс счетчика на основе Redis.
|
8
|
+
|
9
|
+
class BaseCounter
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
KEY_DELIMITER = ':'.freeze
|
13
|
+
VALUE_DELIMITER = ':'.freeze
|
14
|
+
|
15
|
+
attr_reader :redis
|
16
|
+
attr_reader :options
|
17
|
+
attr_reader :params
|
18
|
+
|
19
|
+
# Public: Фабричный метод создания счетчика заданного класса.
|
20
|
+
#
|
21
|
+
# redis - Redis - экземпляр redis - клиента.
|
22
|
+
# opts - Hash - хеш опций счетчика:
|
23
|
+
# counter_name - Symbol/String - идентификатор счетчика.
|
24
|
+
# key_delimiter - String - разделитель ключа (опционально).
|
25
|
+
# value_delimiter - String - разделитель значений (опционально).
|
26
|
+
#
|
27
|
+
# Returns RedisCounters::BaseCounter.
|
28
|
+
#
|
29
|
+
def self.create(redis, opts)
|
30
|
+
counter_class = opts.fetch(:counter_class).to_s.constantize
|
31
|
+
counter_class.new(redis, opts)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Конструктор.
|
35
|
+
#
|
36
|
+
# см. self.create.
|
37
|
+
#
|
38
|
+
# Returns RedisCounters::BaseCounter.
|
39
|
+
#
|
40
|
+
def initialize(redis, opts)
|
41
|
+
@redis = redis
|
42
|
+
@options = opts
|
43
|
+
init
|
44
|
+
end
|
45
|
+
|
46
|
+
# Public: Метод производит обработку события.
|
47
|
+
#
|
48
|
+
# params - Hash - хеш параметров события.
|
49
|
+
#
|
50
|
+
# Returns process_value result.
|
51
|
+
#
|
52
|
+
def process(params = {}, &block)
|
53
|
+
@params = params.with_indifferent_access
|
54
|
+
process_value(&block)
|
55
|
+
end
|
56
|
+
|
57
|
+
def name
|
58
|
+
options[:counter_name]
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :id, :name
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
def init
|
66
|
+
counter_name.present?
|
67
|
+
end
|
68
|
+
|
69
|
+
def counter_name
|
70
|
+
@counter_name ||= options.fetch(:counter_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def key_delimiter
|
74
|
+
@key_delimiter ||= options.fetch(:key_delimiter, KEY_DELIMITER)
|
75
|
+
end
|
76
|
+
|
77
|
+
def value_delimiter
|
78
|
+
@value_delimiter ||= options.fetch(:value_delimiter, VALUE_DELIMITER)
|
79
|
+
end
|
80
|
+
|
81
|
+
def_delegator :redis, :multi, :transaction
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module RedisCounters
|
3
|
+
|
4
|
+
class Bucket
|
5
|
+
|
6
|
+
def self.default_options
|
7
|
+
{:only_leaf => false}
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(counter, bucket_params)
|
11
|
+
@counter = counter
|
12
|
+
@bucket_params = bucket_params.with_indifferent_access
|
13
|
+
|
14
|
+
if bucket_keys.present? && bucket_params.blank? && required?
|
15
|
+
raise ArgumentError, "You must specify a #{self.class.name}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :counter
|
20
|
+
attr_reader :bucket_params
|
21
|
+
|
22
|
+
# Protected: Возвращает букет в виде массива параметров, однозначно его идентифицирующих.
|
23
|
+
#
|
24
|
+
# cluster - Hash - хеш параметров, определяющий букет.
|
25
|
+
# options - Hash - хеш опций:
|
26
|
+
# :only_leaf - Boolean - выбирать только листовые букеты (по умолачнию - true).
|
27
|
+
# Если флаг установлен в true и передана не листовой букет, то
|
28
|
+
# будет сгенерировано исключение KeyError.
|
29
|
+
#
|
30
|
+
# Метод генерирует исключение ArgumentError, если переданы параметры не верно идентифицирующие букет.
|
31
|
+
# Например: ключи группировки счетчика {:param1, :param2, :param3}, а переданы {:param1, :param3}.
|
32
|
+
# Метод генерирует исключение ArgumentError, 'You must specify a cluster',
|
33
|
+
# если букет передан в виде пустого хеша, но группировка используется в счетчике.
|
34
|
+
#
|
35
|
+
# Returns Array.
|
36
|
+
#
|
37
|
+
def params(options = {})
|
38
|
+
options.reverse_merge!(self.class.default_options)
|
39
|
+
|
40
|
+
bucket_keys.inject(Array.new) do |result, key|
|
41
|
+
param = (options[:only_leaf] ? bucket_params.fetch(key) : bucket_params[key])
|
42
|
+
next result unless bucket_params.has_key?(key)
|
43
|
+
next result << param if result.size >= bucket_keys.index(key)
|
44
|
+
|
45
|
+
raise ArgumentError, 'An incorrectly specified %s %s' % [self.class.name, bucket_params]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def bucket_keys
|
52
|
+
raise NotImplementedError.new 'You must specify the grouping key'
|
53
|
+
end
|
54
|
+
|
55
|
+
def required?
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'redis_counters/bucket'
|
4
|
+
|
5
|
+
module RedisCounters
|
6
|
+
|
7
|
+
class Cluster < Bucket
|
8
|
+
def self.default_options
|
9
|
+
{:only_leaf => true}
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def bucket_keys
|
15
|
+
counter.send(:cluster_keys)
|
16
|
+
end
|
17
|
+
|
18
|
+
def required?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'redis_counters/cluster'
|
4
|
+
require 'redis_counters/partition'
|
5
|
+
|
6
|
+
module RedisCounters
|
7
|
+
module ClusterizeAndPartitionize
|
8
|
+
# Public: Возвращает массив партиций (подпартиций) кластера в виде хешей.
|
9
|
+
#
|
10
|
+
# Если партиция не указана, возвращает все партиции кластера.
|
11
|
+
#
|
12
|
+
# params - Hash - хеш параметров, определяющий кластер и партицию.
|
13
|
+
#
|
14
|
+
# Партиция может быть не задана, тогда будут возвращены все партиции кластера.
|
15
|
+
# Может быть задана не листовая партиция, тогда будут все её листовые подпартции.
|
16
|
+
#
|
17
|
+
# Returns Array Of Hash.
|
18
|
+
#
|
19
|
+
def partitions(params = {})
|
20
|
+
partitions_keys(params).map do |part|
|
21
|
+
# parse and exclude counter_name and cluster
|
22
|
+
part = part.split(key_delimiter, -1).from(1).from(cluster_keys.size)
|
23
|
+
# construct hash
|
24
|
+
Hash[partition_keys.zip(part)].with_indifferent_access
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Возвращает данные счетчика для указанной кластера из указанных партиций.
|
29
|
+
#
|
30
|
+
# params - Hash - хеш параметров, определяющий кластер и партицию.
|
31
|
+
#
|
32
|
+
# Партиция может быть не задана, тогда будут возвращены все партиции кластера.
|
33
|
+
# Может быть задана не листовая партиция, тогда будут все её листовые подпартции.
|
34
|
+
#
|
35
|
+
# Если передан блок, то вызывает блок для каждой партиции.
|
36
|
+
# Если блок, не передн, то аккумулирует данные,
|
37
|
+
# из всех запрошенных партиций, и затем возвращает их.
|
38
|
+
#
|
39
|
+
# Returns Array Of Hash.
|
40
|
+
#
|
41
|
+
def data(params = {})
|
42
|
+
total_rows = 0
|
43
|
+
cluster = ::RedisCounters::Cluster.new(self, params).params
|
44
|
+
parts = partitions(params).map { |partition| ::RedisCounters::Partition.new(self, partition).params }
|
45
|
+
|
46
|
+
result = parts.flat_map do |partition|
|
47
|
+
rows = partition_data(cluster, partition)
|
48
|
+
total_rows += rows.size
|
49
|
+
block_given? ? yield(rows) : rows
|
50
|
+
end
|
51
|
+
|
52
|
+
block_given? ? total_rows : result
|
53
|
+
end
|
54
|
+
|
55
|
+
# Public: Транзакционно удаляет данные указанной партиций или всех её подпартиций.
|
56
|
+
#
|
57
|
+
# params - Hash - хеш параметров, определяющий кластер и партицию.
|
58
|
+
#
|
59
|
+
# Партиция может быть не задана, тогда будут возвращены все партиции кластера.
|
60
|
+
# Может быть задана не листовая партиция, тогда будут все её листовые подпартции.
|
61
|
+
#
|
62
|
+
# Если передан блок, то вызывает блок, после удаления всех данных, в транзакции.
|
63
|
+
#
|
64
|
+
# Returns Nothing.
|
65
|
+
#
|
66
|
+
def delete_partitions!(params = {})
|
67
|
+
parts = partitions(params)
|
68
|
+
|
69
|
+
transaction do
|
70
|
+
parts.each { |partition| delete_partition_direct!(params.merge(partition)) }
|
71
|
+
yield if block_given?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Public: Транзакционно удаляет все данные счетчика в кластере.
|
76
|
+
# Если кластеризация не используется, то удаляет все данные.
|
77
|
+
#
|
78
|
+
# cluster - Hash - хеш параметров, определяющих кластер.
|
79
|
+
# Опционально, если кластеризация не используется.
|
80
|
+
#
|
81
|
+
# Если передан блок, то вызывает блок, после удаления всех данных, в транзакции.
|
82
|
+
#
|
83
|
+
# Returns Nothing.
|
84
|
+
#
|
85
|
+
def delete_all!(cluster = {})
|
86
|
+
parts = partitions(cluster)
|
87
|
+
|
88
|
+
transaction do
|
89
|
+
delete_all_direct!(cluster, redis, parts)
|
90
|
+
yield if block_given?
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Public: Нетранзакционно удаляет данные конкретной конечной партиции.
|
95
|
+
#
|
96
|
+
# params - Hash - хеш параметров, определяющий кластер и листовую партицию.
|
97
|
+
#
|
98
|
+
# write_session - Redis - соединение с Redis, в рамках которого
|
99
|
+
# будет производится удаление (опционально).
|
100
|
+
# По умолчанию - основное соединение счетчика.
|
101
|
+
#
|
102
|
+
# Должна быть задана конкретная листовая партиция.
|
103
|
+
#
|
104
|
+
# Returns Nothing.
|
105
|
+
#
|
106
|
+
def delete_partition_direct!(params = {}, write_session = redis)
|
107
|
+
cluster = ::RedisCounters::Cluster.new(self, params).params
|
108
|
+
partition = ::RedisCounters::Partition.new(self, params).params(:only_leaf => true)
|
109
|
+
key = key(partition, cluster)
|
110
|
+
write_session.del(key)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Public: Нетранзакционно удаляет все данные счетчика в кластере.
|
114
|
+
# Если кластеризация не используется, то удаляет все данные.
|
115
|
+
#
|
116
|
+
# cluster - Hash - хеш параметров, определяющих кластер.
|
117
|
+
# write_session - Redis - соединение с Redis, в рамках которого
|
118
|
+
# будет производится удаление (опционально).
|
119
|
+
# По умолчанию - основное соединение счетчика.
|
120
|
+
#
|
121
|
+
# Returns Nothing.
|
122
|
+
#
|
123
|
+
def delete_all_direct!(cluster, write_session = redis, parts = partitions(cluster))
|
124
|
+
parts.each do |partition|
|
125
|
+
delete_partition_direct!(cluster.merge(partition), write_session)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
def key(partition = partition_params, cluster = cluster_params)
|
132
|
+
raise 'Array required' if partition && !partition.is_a?(Array)
|
133
|
+
raise 'Array required' if cluster && !cluster.is_a?(Array)
|
134
|
+
|
135
|
+
[counter_name, cluster, partition].flatten.join(key_delimiter)
|
136
|
+
end
|
137
|
+
|
138
|
+
def cluster_params
|
139
|
+
cluster_keys.map { |key| params.fetch(key) }
|
140
|
+
end
|
141
|
+
|
142
|
+
def partition_params
|
143
|
+
partition_keys.map do |key|
|
144
|
+
key.respond_to?(:call) ? key.call(params) : params.fetch(key)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def cluster_keys
|
149
|
+
@cluster_keys ||= Array.wrap(options.fetch(:cluster_keys, []))
|
150
|
+
end
|
151
|
+
|
152
|
+
def partition_keys
|
153
|
+
@partition_keys ||= Array.wrap(options.fetch(:partition_keys, []))
|
154
|
+
end
|
155
|
+
|
156
|
+
def use_partitions?
|
157
|
+
partition_keys.present?
|
158
|
+
end
|
159
|
+
|
160
|
+
def set_params(params)
|
161
|
+
@params = params.with_indifferent_access
|
162
|
+
check_cluster_params
|
163
|
+
end
|
164
|
+
|
165
|
+
def form_cluster_params(cluster_params = params)
|
166
|
+
RedisCounters::Cluster.new(self, cluster_params).params
|
167
|
+
end
|
168
|
+
|
169
|
+
alias_method :check_cluster_params, :form_cluster_params
|
170
|
+
|
171
|
+
# Protected: Возвращает массив листовых партиций в виде ключей.
|
172
|
+
#
|
173
|
+
# params - Hash - хеш параметров, определяющий кластер и партицию.
|
174
|
+
#
|
175
|
+
# Если кластер не указан и нет кластеризации в счетчике, то возвращает все партиции.
|
176
|
+
# Партиция может быть не задана, тогда будут возвращены все партиции кластера (все партиции, если нет кластеризации).
|
177
|
+
# Может быть задана не листовая партиция, тогда будут все её листовые подпартции.
|
178
|
+
#
|
179
|
+
# Returns Array of Hash.
|
180
|
+
#
|
181
|
+
def partitions_keys(params = {})
|
182
|
+
cluster = ::RedisCounters::Cluster.new(self, params).params
|
183
|
+
partition = ::RedisCounters::Partition.new(self, params).params
|
184
|
+
|
185
|
+
strict_pattern = key(partition, cluster) if (cluster.present? && partition_keys.blank?) || partition.present?
|
186
|
+
fuzzy_pattern = key(partition << '*', cluster)
|
187
|
+
|
188
|
+
result = []
|
189
|
+
result |= redis.keys(strict_pattern) if strict_pattern.present?
|
190
|
+
result |= redis.keys(fuzzy_pattern) if fuzzy_pattern.present?
|
191
|
+
result
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'redis_counters/base_counter'
|
3
|
+
require 'redis_counters/clusterize_and_partitionize'
|
4
|
+
|
5
|
+
module RedisCounters
|
6
|
+
|
7
|
+
# Счетчик на основе redis-hash, с возможностью партиционирования и кластеризации значений.
|
8
|
+
|
9
|
+
class HashCounter < BaseCounter
|
10
|
+
include ClusterizeAndPartitionize
|
11
|
+
|
12
|
+
alias_method :increment, :process
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def process_value
|
17
|
+
redis.hincrbyfloat(key, field, params.fetch(:value, 1.0))
|
18
|
+
end
|
19
|
+
|
20
|
+
def field
|
21
|
+
if group_keys.present?
|
22
|
+
group_params = group_keys.map { |key| params.fetch(key) }
|
23
|
+
else
|
24
|
+
group_params = [field_name]
|
25
|
+
end
|
26
|
+
|
27
|
+
group_params.join(value_delimiter)
|
28
|
+
end
|
29
|
+
|
30
|
+
def field_name
|
31
|
+
@field_name ||= options.fetch(:field_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def group_keys
|
35
|
+
@group_keys ||= Array.wrap(options.fetch(:group_keys, []))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Protected: Возвращает данные партиции в виде массива хешей.
|
39
|
+
#
|
40
|
+
# Каждый элемент массива, представлен в виде хеша, содержащего все параметры кластеризации и
|
41
|
+
# значение счетчика в ключе :value.
|
42
|
+
#
|
43
|
+
# cluster - Array - листовой кластер - массив параметров однозначно идентифицирующий кластер.
|
44
|
+
# partition - Array - листовая партиция - массив параметров однозначно идентифицирующий партицию.
|
45
|
+
#
|
46
|
+
# Returns Array of WithIndifferentAccess.
|
47
|
+
#
|
48
|
+
def partition_data(cluster, partition)
|
49
|
+
keys = group_keys.dup << :value
|
50
|
+
redis.hgetall(key(partition, cluster)).inject(Array.new) do |result, (key, value)|
|
51
|
+
values = key.split(value_delimiter, -1) << format_value(value)
|
52
|
+
values = values.from(1) unless group_keys.present?
|
53
|
+
result << Hash[keys.zip(values)].with_indifferent_access
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def format_value(value)
|
58
|
+
if float_mode?
|
59
|
+
value.to_f
|
60
|
+
else
|
61
|
+
value.to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def float_mode?
|
66
|
+
@float_mode ||= options.fetch(:float_mode, false)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|