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