redis_counters 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/CHANGELOG.md +38 -0
  4. data/Gemfile +3 -0
  5. data/Makefile +14 -0
  6. data/README.md +320 -0
  7. data/Rakefile +15 -0
  8. data/lib/redis_counters.rb +21 -0
  9. data/lib/redis_counters/base_counter.rb +84 -0
  10. data/lib/redis_counters/bucket.rb +59 -0
  11. data/lib/redis_counters/cluster.rb +22 -0
  12. data/lib/redis_counters/clusterize_and_partitionize.rb +194 -0
  13. data/lib/redis_counters/hash_counter.rb +70 -0
  14. data/lib/redis_counters/partition.rb +16 -0
  15. data/lib/redis_counters/unique_hash_counter.rb +51 -0
  16. data/lib/redis_counters/unique_values_lists/base.rb +57 -0
  17. data/lib/redis_counters/unique_values_lists/blocking.rb +167 -0
  18. data/lib/redis_counters/unique_values_lists/expirable.rb +155 -0
  19. data/lib/redis_counters/unique_values_lists/non_blocking.rb +91 -0
  20. data/lib/redis_counters/version.rb +3 -0
  21. data/redis_counters.gemspec +31 -0
  22. data/spec/redis_counters/base_spec.rb +29 -0
  23. data/spec/redis_counters/hash_counter_spec.rb +462 -0
  24. data/spec/redis_counters/unique_hash_counter_spec.rb +83 -0
  25. data/spec/redis_counters/unique_values_lists/blocking_spec.rb +94 -0
  26. data/spec/redis_counters/unique_values_lists/expirable_spec.rb +6 -0
  27. data/spec/redis_counters/unique_values_lists/non_blicking_spec.rb +6 -0
  28. data/spec/spec_helper.rb +24 -0
  29. data/spec/support/unique_values_lists/common.rb +563 -0
  30. data/spec/support/unique_values_lists/expirable.rb +162 -0
  31. data/spec/support/unique_values_lists/set.rb +119 -0
  32. data/tasks/audit.rake +6 -0
  33. data/tasks/cane.rake +12 -0
  34. data/tasks/changelog.rake +7 -0
  35. data/tasks/coverage.rake +21 -0
  36. data/tasks/support.rb +24 -0
  37. 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
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+
3
+ require 'redis_counters/bucket'
4
+
5
+ module RedisCounters
6
+
7
+ class Partition < Bucket
8
+ def self.default_options
9
+ {:only_leaf => false}
10
+ end
11
+
12
+ def bucket_keys
13
+ counter.send(:partition_keys)
14
+ end
15
+ end
16
+ end