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,51 @@
1
+ # coding: utf-8
2
+ require 'redis_counters/hash_counter'
3
+
4
+ module RedisCounters
5
+
6
+ # HashCounter, с возможностью подсчета только уникальных значений.
7
+
8
+ class UniqueHashCounter < HashCounter
9
+ UNIQUE_LIST_POSTFIX = 'uq'.freeze
10
+
11
+ UNIQUE_LIST_POSTFIX_DELIMITER = '_'.freeze
12
+
13
+ attr_reader :unique_values_list
14
+
15
+ protected
16
+
17
+ def process_value
18
+ unique_values_list.add(params) { super }
19
+ end
20
+
21
+ def init
22
+ super
23
+ @unique_values_list = unique_values_list_class.new(
24
+ redis,
25
+ unique_values_list_options
26
+ )
27
+ end
28
+
29
+ def unique_values_list_options
30
+ options.fetch(:unique_list).merge!(:counter_name => unique_values_list_name)
31
+ end
32
+
33
+ def unique_values_list_name
34
+ [counter_name, UNIQUE_LIST_POSTFIX].join(unique_list_postfix_delimiter)
35
+ end
36
+
37
+ def unique_values_list_class
38
+ unique_values_list_options.fetch(:list_class).to_s.constantize
39
+ end
40
+
41
+ def unique_list_postfix_delimiter
42
+ @unique_list_postfix_delimiter ||= options.fetch(:unique_list_postfix_delimiter, UNIQUE_LIST_POSTFIX_DELIMITER)
43
+ end
44
+
45
+ def partitions_keys(params = {})
46
+ # удаляем из списка партиций, ключи в которых хранятся списки уникальных значений
47
+ super.delete_if { |partition| partition.start_with?(unique_values_list_name) }
48
+ end
49
+ end
50
+
51
+ end
@@ -0,0 +1,57 @@
1
+ # coding: utf-8
2
+ require 'redis_counters/base_counter'
3
+ require 'redis_counters/clusterize_and_partitionize'
4
+
5
+ module RedisCounters
6
+ module UniqueValuesLists
7
+
8
+ # Базовый класс списка уникальных значений,
9
+ # с возможностью кластеризации и партиционирования.
10
+
11
+ class Base < RedisCounters::BaseCounter
12
+ include RedisCounters::ClusterizeAndPartitionize
13
+
14
+ alias_method :add, :process
15
+ alias_method :<<, :process
16
+
17
+ # Public: Проверяет существует ли заданное значение.
18
+ #
19
+ # params - Hash - параметры кластера и значения.
20
+ #
21
+ # Returns Boolean.
22
+ #
23
+ def has_value?(params)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ protected
28
+
29
+ def value
30
+ value_params = value_keys.map { |key| params.fetch(key) }
31
+ value_params.join(value_delimiter)
32
+ end
33
+
34
+ def value_keys
35
+ @value_keys ||= Array.wrap(options.fetch(:value_keys))
36
+ end
37
+
38
+ # Protected: Возвращает данные партиции в виде массива хешей.
39
+ #
40
+ # Каждый элемент массива, представлен в виде хеша, содержащего все параметры уникального значения.
41
+ #
42
+ # cluster - Array - листовой кластер - массив параметров однозначно идентифицирующий кластер.
43
+ # partition - Array - листовая партиция - массив параметров однозначно идентифицирующий партицию.
44
+ #
45
+ # Returns Array of WithIndifferentAccess.
46
+ #
47
+ def partition_data(cluster, partition)
48
+ keys = value_keys
49
+ redis.smembers(key(partition, cluster)).inject(Array.new) do |result, (key, value)|
50
+ values = key.split(value_delimiter, -1) << value.to_i
51
+ result << Hash[keys.zip(values)].with_indifferent_access
52
+ end
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,167 @@
1
+ # coding: utf-8
2
+ require 'redis_counters/unique_values_lists/base'
3
+
4
+ module RedisCounters
5
+ module UniqueValuesLists
6
+
7
+ # Список уникальных значений, на основе механизма оптимистических блокировок.
8
+ #
9
+ # смотри Optimistic locking using check-and-set:
10
+ # http://redis.io/topics/transactions
11
+ #
12
+ # Особенности:
13
+ # * Значения сохраняет в партициях;
14
+ # * Ведет список партиций;
15
+ # * Полностью транзакционен.
16
+
17
+ class Blocking < Base
18
+ PARTITIONS_LIST_POSTFIX = :partitions
19
+
20
+ # Public: Проверяет существует ли заданное значение.
21
+ #
22
+ # params - Hash - параметры кластера и значения.
23
+ #
24
+ # Returns Boolean.
25
+ #
26
+ def has_value?(params)
27
+ set_params(params)
28
+ reset_partitions_cache
29
+ value_already_exists?
30
+ end
31
+
32
+ # Public: Нетранзакционно удаляет данные конкретной конечной партиции.
33
+ #
34
+ # params - Hash - хеш параметров, определяющий кластер и партицию.
35
+ # write_session - Redis - соединение с Redis, в рамках которого
36
+ # будет производится удаление (опционально).
37
+ # По умолчанию - основное соединение счетчика.
38
+ #
39
+ # Если передан блок, то вызывает блок, после удаления всех данных, в транзакции.
40
+ #
41
+ # Returns Nothing.
42
+ #
43
+ def delete_partition_direct!(params = {}, write_session = redis)
44
+ super(params, write_session)
45
+
46
+ # удаляем партицию из списка
47
+ return unless use_partitions?
48
+
49
+ cluster = ::RedisCounters::Cluster.new(self, params).params
50
+ partition = ::RedisCounters::Partition.new(self, params).params(:only_leaf => true)
51
+
52
+ partition = partition.flatten.join(key_delimiter)
53
+ write_session.lrem(partitions_list_key(cluster), 0, partition)
54
+ end
55
+
56
+ protected
57
+
58
+ def key(partition = partition_params, cluster = cluster_params)
59
+ return super if use_partitions?
60
+
61
+ raise 'Array required' if partition && !partition.is_a?(Array)
62
+ raise 'Array required' if cluster && !cluster.is_a?(Array)
63
+
64
+ [counter_name, cluster, partition].flatten.compact.join(key_delimiter)
65
+ end
66
+
67
+ def process_value
68
+ loop do
69
+ reset_partitions_cache
70
+
71
+ watch_partitions_list
72
+ watch_all_partitions
73
+
74
+ if value_already_exists?
75
+ redis.unwatch
76
+ return false
77
+ end
78
+
79
+ result = transaction do
80
+ add_value
81
+ add_partition
82
+ yield redis if block_given?
83
+ end
84
+
85
+ return true if result.present?
86
+ end
87
+ end
88
+
89
+ def reset_partitions_cache
90
+ @partitions = nil
91
+ end
92
+
93
+ def watch_partitions_list
94
+ return unless use_partitions?
95
+ redis.watch(partitions_list_key)
96
+ end
97
+
98
+ def watch_all_partitions
99
+ all_partitions.each do |partition|
100
+ redis.watch(key(partition))
101
+ end
102
+ end
103
+
104
+ def value_already_exists?
105
+ all_partitions.reverse.any? do |partition|
106
+ redis.sismember(key(partition), value)
107
+ end
108
+ end
109
+
110
+ def add_value
111
+ redis.sadd(key, value)
112
+ end
113
+
114
+ def all_partitions(cluster = cluster_params)
115
+ return @partitions unless @partitions.nil?
116
+ return (@partitions = [nil]) unless use_partitions?
117
+
118
+ @partitions = redis.lrange(partitions_list_key(cluster), 0, -1)
119
+ @partitions = @partitions.map do |partition|
120
+ partition.split(key_delimiter, -1)
121
+ end.delete_if(&:empty?)
122
+ end
123
+
124
+ def add_partition
125
+ return unless use_partitions?
126
+ return unless new_partition?
127
+ redis.rpush(partitions_list_key, current_partition)
128
+ end
129
+
130
+ def partitions_list_key(cluster = cluster_params)
131
+ raise 'Array required' if cluster && !cluster.is_a?(Array)
132
+
133
+ [counter_name, cluster, PARTITIONS_LIST_POSTFIX].flatten.join(key_delimiter)
134
+ end
135
+
136
+ def current_partition
137
+ partition_params.flatten.join(key_delimiter)
138
+ end
139
+
140
+ def new_partition?
141
+ !all_partitions.include?(current_partition.split(key_delimiter))
142
+ end
143
+
144
+
145
+ # Protected: Возвращает массив листовых партиций в виде ключей.
146
+ #
147
+ # Если кластер не указан и нет кластеризации в счетчике, то возвращает все партиции.
148
+ # Если партиция не указана, возвращает все партиции кластера (все партиции, если нет кластеризации).
149
+ #
150
+ # params - Hash - хеш параметров, определяющий кластер и партицию.
151
+ #
152
+ # Returns Array of Hash.
153
+ #
154
+ def partitions_keys(params = {})
155
+ reset_partitions_cache
156
+
157
+ cluster = ::RedisCounters::Cluster.new(self, params).params
158
+ partition = ::RedisCounters::Partition.new(self, params).params
159
+
160
+ partitions_keys = all_partitions(cluster).map { |part| key(part, cluster) }
161
+
162
+ fuzzy_pattern = key(partition, cluster)
163
+ partitions_keys.select { |part| part.start_with?(fuzzy_pattern) }
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,155 @@
1
+ # coding: utf-8
2
+
3
+ require 'redis_counters/unique_values_lists/blocking'
4
+ require 'active_support/core_ext/module/aliasing'
5
+
6
+ module RedisCounters
7
+ module UniqueValuesLists
8
+
9
+ # Список уникальных значений, с возможностью expire отдельных элементов.
10
+ #
11
+ # На основе сортированного множества.
12
+ # http://redis4you.com/code.php?id=010
13
+ #
14
+ # На основе механизма оптимистических блокировок.
15
+ # смотри Optimistic locking using check-and-set:
16
+ # http://redis.io/topics/transactions
17
+ #
18
+ # Особенности:
19
+ # * Expire - таймаут, можно установить как на уровне счетчика,
20
+ # так и на уровне отдельного занчения;
21
+ # * Очистка возможна как в автоматическогом режиме так в и ручном;
22
+ # * Значения сохраняет в партициях;
23
+ # * Ведет список партиций;
24
+ # * Полностью транзакционен.
25
+ #
26
+ # Пример:
27
+ #
28
+ # counter = RedisCounters::UniqueValuesLists::Expirable.new(redis,
29
+ # :counter_name => :sessions,
30
+ # :value_keys => [:session_id],
31
+ # :expire => 10.minutes
32
+ # )
33
+ #
34
+ # counter << session_id: 1
35
+ # counter << session_id: 2
36
+ # counter << session_id: 3, expire: :never
37
+ #
38
+ # counter.data
39
+ # > [{session_id: 1}, {session_id: 2}, {session_id: 3}]
40
+ #
41
+ # # after 10 minutes
42
+ #
43
+ # counter.data
44
+ # > [{session_id: 3}]
45
+ #
46
+ # counter.has_value?(session_id: 1)
47
+ # false
48
+
49
+ class Expirable < Blocking
50
+ DEFAULT_AUTO_CLEAN_EXPIRED = true
51
+ DEFAULT_VALUE_TIMEOUT = :never
52
+
53
+ NEVER_EXPIRE_TIMESTAMP = 0
54
+
55
+ # Public: Производит принудительную очистку expired - значений.
56
+ #
57
+ # cluster - Hash - параметры кластера, если используется кластеризация.
58
+ #
59
+ # Returns nothing.
60
+ #
61
+ def clean_expired(cluster = {})
62
+ set_params(cluster)
63
+ internal_clean_expired
64
+ end
65
+
66
+ protected
67
+
68
+ def add_value
69
+ redis.zadd(key, value_expire_timestamp, value)
70
+ end
71
+
72
+ def reset_partitions_cache
73
+ super
74
+ internal_clean_expired if auto_clean_expired?
75
+ end
76
+
77
+ alias_method :clean, :reset_partitions_cache
78
+
79
+ def current_timestamp
80
+ Time.now.to_i
81
+ end
82
+
83
+ def value_already_exists?
84
+ all_partitions.reverse.any? do |partition|
85
+ redis.zrank(key(partition), value).present?
86
+ end
87
+ end
88
+
89
+ def internal_clean_expired
90
+ all_partitions.each do |partition|
91
+ redis.zremrangebyscore(key(partition), "(#{NEVER_EXPIRE_TIMESTAMP}", current_timestamp)
92
+ end
93
+ end
94
+
95
+ def value_expire_timestamp
96
+ timeout = params[:expire] || default_value_expire
97
+
98
+ case timeout
99
+ when Symbol
100
+ NEVER_EXPIRE_TIMESTAMP
101
+ else
102
+ current_timestamp + timeout.to_i
103
+ end
104
+ end
105
+
106
+ def default_value_expire
107
+ @default_value_expire ||= options[:expire].try(:seconds) || DEFAULT_VALUE_TIMEOUT
108
+ end
109
+
110
+ def auto_clean_expired?
111
+ @auto_clean_expired ||= options.fetch(:clean_expired, DEFAULT_AUTO_CLEAN_EXPIRED)
112
+ end
113
+
114
+ def partitions_with_clean(params = {})
115
+ clean_empty_partitions(params)
116
+ partitions_without_clean(params)
117
+ end
118
+
119
+ alias_method_chain :partitions, :clean
120
+
121
+ # Protected: Производит очистку expired - значений и пустых партиций.
122
+ #
123
+ # params - Hash - параметры кластера, если используется кластеризация.
124
+ #
125
+ # Returns nothing.
126
+ #
127
+ def clean_empty_partitions(params)
128
+ set_params(params)
129
+ clean
130
+
131
+ partitions_without_clean(params).each do |partition|
132
+ next if redis.zcard(key(partition.values)).nonzero?
133
+ delete_partition_direct!(params.merge(partition))
134
+ end
135
+ end
136
+
137
+ # Protected: Возвращает данные партиции в виде массива хешей.
138
+ #
139
+ # Каждый элемент массива, представлен в виде хеша, содержащего все параметры уникального значения.
140
+ #
141
+ # cluster - Array - листовой кластер - массив параметров однозначно идентифицирующий кластер.
142
+ # partition - Array - листовая партиция - массив параметров однозначно идентифицирующий партицию.
143
+ #
144
+ # Returns Array of WithIndifferentAccess.
145
+ #
146
+ def partition_data(cluster, partition)
147
+ keys = value_keys
148
+ redis.zrangebyscore(key(partition, cluster), '-inf', '+inf').inject(Array.new) do |result, (key, value)|
149
+ values = key.split(value_delimiter, -1) << value.to_i
150
+ result << Hash[keys.zip(values)].with_indifferent_access
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,91 @@
1
+ # coding: utf-8
2
+ require 'redis_counters/unique_values_lists/base'
3
+
4
+ module RedisCounters
5
+ module UniqueValuesLists
6
+
7
+ # Список уникальных значений, на основе не блокирующего алгоритма.
8
+ #
9
+ # Особенности:
10
+ # * 2-х кратный расход памяти в случае использования партиций;
11
+ # * Не ведет список партиций;
12
+ # * Не транзакционен;
13
+ # * Методы delete_partitions! и delete_partition_direct!, удаляют только дублирующие партиции,
14
+ # но не удаляют данные из основной партиции.
15
+ # Для удаления основной партиции необходимо вызвать delete_main_partition!
16
+ # или воспользоваться методами delete_all! или delete_all_direct!,
17
+ # для удаления всех партиций кластера включая основную.
18
+
19
+ class NonBlocking < Base
20
+
21
+ # Public: Проверяет существует ли заданное значение.
22
+ #
23
+ # params - Hash - параметры кластера и значения.
24
+ #
25
+ # Returns Boolean.
26
+ #
27
+ def has_value?(params)
28
+ set_params(params)
29
+ redis.sismember(main_partition_key, value)
30
+ end
31
+
32
+ # Public: Нетранзакционно удаляет все данные счетчика в кластере, включая основную партицию.
33
+ # Если кластеризация не используется, то удаляет все данные.
34
+ #
35
+ # cluster - Hash - хеш параметров, определяющих кластер.
36
+ # write_session - Redis - соединение с Redis, в рамках которого
37
+ # будет производится удаление (опционально).
38
+ # По умолчанию - основное соединение счетчика.
39
+ #
40
+ # Returns Nothing.
41
+ #
42
+ def delete_all_direct!(cluster, write_session = redis, parts = partitions(cluster))
43
+ super(cluster, write_session, parts)
44
+ delete_main_partition!(cluster, write_session)
45
+ end
46
+
47
+ # Public: Удаляет основную партицию.
48
+ #
49
+ # cluster - Hash - хеш параметров, определяющих кластер.
50
+ # Опционально, если кластеризация не используется.
51
+ # write_session - Redis - соединение с Redis, в рамках которого
52
+ # будет производится удаление (опционально).
53
+ # По умолчанию - основное соединение счетчика.
54
+ #
55
+ # Returns Nothing.
56
+ #
57
+ def delete_main_partition!(cluster = {}, write_session = redis)
58
+ cluster = ::RedisCounters::Cluster.new(self, cluster).params
59
+ key = key(main_partition, cluster)
60
+ write_session.del(key)
61
+ end
62
+
63
+ protected
64
+
65
+ def process_value
66
+ return unless add_value
67
+ yield redis if block_given?
68
+ true
69
+ end
70
+
71
+ def add_value
72
+ return unless redis.sadd(main_partition_key, value)
73
+ redis.sadd(current_partition_key, value) if use_partitions?
74
+ true
75
+ end
76
+
77
+ def main_partition_key
78
+ key(main_partition)
79
+ end
80
+
81
+ def current_partition_key
82
+ key
83
+ end
84
+
85
+ def main_partition
86
+ []
87
+ end
88
+ end
89
+
90
+ end
91
+ end