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.
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