redis_counters 1.3.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 +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
|