From b29eab7814d9c04197fe67cd41a37833b4984ad4 Mon Sep 17 00:00:00 2001 From: Artem Napolskih Date: Thu, 10 Oct 2013 17:27:51 +0600 Subject: [PATCH] feature(unique_values_lists): added methods to read and delete data --- lib/redis_counters/base_counter.rb | 16 +- lib/redis_counters/hash_counter.rb | 59 +- lib/redis_counters/unique_hash_counter.rb | 13 +- .../unique_values_lists/base.rb | 195 +++++- .../unique_values_lists/fast.rb | 56 +- .../unique_values_lists/standard.rb | 65 +- spec/redis_counters/hash_counter_spec.rb | 89 ++- .../unique_hash_counter_spec.rb | 30 +- .../unique_values_lists/standard_spec.rb | 20 +- spec/support/unique_values_lists.rb | 598 +++++++++++++++--- 10 files changed, 974 insertions(+), 167 deletions(-) diff --git a/lib/redis_counters/base_counter.rb b/lib/redis_counters/base_counter.rb index 68edc5d..9677aa1 100644 --- a/lib/redis_counters/base_counter.rb +++ b/lib/redis_counters/base_counter.rb @@ -12,12 +12,6 @@ class BaseCounter KEY_DELIMITER = ':'.freeze VALUE_DELIMITER = ':'.freeze - class_attribute :key_delimiter - class_attribute :value_delimiter - - self.key_delimiter = KEY_DELIMITER - self.value_delimiter = VALUE_DELIMITER - attr_reader :redis attr_reader :options attr_reader :params @@ -60,6 +54,12 @@ def process(params = {}, &block) process_value(&block) end + def name + options[:counter_name] + end + + alias_method :id, :name + protected def init @@ -71,11 +71,11 @@ def counter_name end def key_delimiter - @key_delimiter ||= options.fetch(:key_delimiter, self.class.key_delimiter) + @key_delimiter ||= options.fetch(:key_delimiter, KEY_DELIMITER) end def value_delimiter - @value_delimiter ||= options.fetch(:value_delimiter, self.class.value_delimiter) + @value_delimiter ||= options.fetch(:value_delimiter, VALUE_DELIMITER) end def_delegator :redis, :multi, :transaction diff --git a/lib/redis_counters/hash_counter.rb b/lib/redis_counters/hash_counter.rb index 65a2fa5..75560b6 100644 --- a/lib/redis_counters/hash_counter.rb +++ b/lib/redis_counters/hash_counter.rb @@ -20,9 +20,9 @@ class HashCounter < BaseCounter def partitions(parts = {}) partitions_raw(parts).map do |part| # parse and exclude counter_name - part = part.split(key_delimiter).from(1) + part = part.split(key_delimiter, -1).from(1) # construct hash - HashWithIndifferentAccess[partition_keys.zip(part)] + Hash[partition_keys.zip(part)].with_indifferent_access end end @@ -38,12 +38,8 @@ def partitions(parts = {}) # Returns Array Of Hash. # def data(parts = {}) - # получаем все подпартиции parts = partitions(parts) - # подгатавливаем в необходимом виде - parts = prepared_parts(parts) - - parts.flat_map do |partition| + prepared_parts(parts).flat_map do |partition| rows = partition_data(partition) block_given? ? yield(rows) : rows end @@ -88,7 +84,7 @@ def delete_partitions!(parts) # Returns Nothing. # def delete_partition_direct!(partition, write_session = redis) - partition = prepared_parts(partition, true) + partition = prepared_parts(partition, :only_leaf => true) key = key(partition) write_session.del(key) end @@ -131,9 +127,7 @@ def partition_keys @partition_keys ||= Array.wrap(options.fetch(:partition_keys, [])) end - # Public: Возвращает массив партиций (подпартиций) в виде ключей. - # - # Если партиция не указана, возвращает все партиции. + # Protected: Возвращает массив листовых партиций в виде ключей. # # parts - Array of Hash - список партиций (опционально). # По умолчанию, выбираются все данные. @@ -141,20 +135,38 @@ def partition_keys # Returns Array of Hash. # def partitions_raw(parts = {}) - prepared_parts(parts).flat_map do |partition| + prepared_parts(parts).inject(Array.new) do |result, partition| strict_pattern = key(partition) fuzzy_pattern = key(partition << '*') - redis.keys(strict_pattern) | redis.keys(fuzzy_pattern) + result |= redis.keys(strict_pattern) if strict_pattern.present? + result |= redis.keys(fuzzy_pattern) if fuzzy_pattern.present? + result end - .uniq end - def prepared_parts(parts, only_leaf = false) + # Protected: Возвращает массив партиций, где каждая партиция, + # представляет собой массив параметров, однозначно её идентифицирующих. + # + # parts - Array of Hash - список партиций. + # options - Hash - хеш опций: + # :only_leaf - Boolean - выбирать только листовые партиции (по умолачнию - false). + # Если флаг установлен в true и передана не листовая партиция, то + # будет сгенерировано исключение KeyError. + # + # Метод генерирует исключение ArgumentError, если переданы параметры не верно идентифицирующие партицию. + # Например: ключи партиционирования счетчика {:param1, :param2, :param3}, а переданы {:param1, :param3}. + # + # Returns Array of Array. + # + def prepared_parts(parts, options = {}) + default_options = {:only_leaf => false} + options.reverse_merge!(default_options) + parts = Array.wrap(parts).map(&:with_indifferent_access) parts.map do |partition| partition_keys.inject(Array.new) do |result, key| - param = (only_leaf ? partition.fetch(key) : partition[key]) - next result if param.nil? + param = (options[:only_leaf] ? partition.fetch(key) : partition[key]) + next result unless partition.has_key?(key) next result << param if result.size >= partition_keys.index(key) raise ArgumentError, 'An incorrectly specified partition %s' % [partition] @@ -162,12 +174,21 @@ def prepared_parts(parts, only_leaf = false) end end + # Protected: Возвращает данные партиции в виде массива хешей. + # + # Каждый элемент массива, представлен в виде хеша, содержащего все параметры группировки и + # значение счетчика в ключе :value. + # + # partition - Array - листовая партиция - массив параметров однозначно идентифицирующий партицию. + # + # Returns Array of WithIndifferentAccess. + # def partition_data(partition) keys = group_keys.dup << :value redis.hgetall(key(partition)).inject(Array.new) do |result, (key, value)| - values = key.split(value_delimiter) << value.to_i + values = key.split(value_delimiter, -1) << value.to_i values = values.from(1) unless group_keys.present? - result << HashWithIndifferentAccess[keys.zip(values)] + result << Hash[keys.zip(values)].with_indifferent_access end end end diff --git a/lib/redis_counters/unique_hash_counter.rb b/lib/redis_counters/unique_hash_counter.rb index da04a2a..b2be2fa 100644 --- a/lib/redis_counters/unique_hash_counter.rb +++ b/lib/redis_counters/unique_hash_counter.rb @@ -10,9 +10,7 @@ class UniqueHashCounter < HashCounter UNIQUE_LIST_POSTFIX_DELIMITER = '_'.freeze - class_attribute :unique_list_postfix_delimiter - - self.unique_list_postfix_delimiter = UNIQUE_LIST_POSTFIX_DELIMITER + attr_reader :unique_values_list protected @@ -20,8 +18,6 @@ def process_value unique_values_list.add(params) { super } end - attr_reader :unique_values_list - def init super @unique_values_list = unique_values_list_class.new( @@ -43,7 +39,12 @@ def unique_values_list_class end def unique_list_postfix_delimiter - @unique_list_postfix_delimiter ||= options.fetch(:unique_list_postfix_delimiter, self.class.unique_list_postfix_delimiter) + @unique_list_postfix_delimiter ||= options.fetch(:unique_list_postfix_delimiter, UNIQUE_LIST_POSTFIX_DELIMITER) + end + + def partitions_raw(parts = {}) + # удаляем из списка партиций, ключи в которых хранятся списки уникальных значений + super.delete_if { |partition| partition.start_with?(unique_values_list_name) } end end diff --git a/lib/redis_counters/unique_values_lists/base.rb b/lib/redis_counters/unique_values_lists/base.rb index ea231fb..b05ad73 100644 --- a/lib/redis_counters/unique_values_lists/base.rb +++ b/lib/redis_counters/unique_values_lists/base.rb @@ -4,16 +4,123 @@ module RedisCounters module UniqueValuesLists - # Базовый класс уникального списка значений, + # Базовый класс списка уникальных значений, # с возможностью группировки и партиционирования. class Base < RedisCounters::BaseCounter alias_method :add, :process + # Public: Возвращает данные счетчика для указанной группы из указанных партиций. + # + # group - Hash - группа (опционально, если не используются группы). + # parts - Array of Hash - список партиций (опционально). + # По умолчанию, выбираются все данные группы. + # + # Если передан блок, то вызывает блок для каждой партиции. + # Если блок, не передн, то аккумулирует данные, + # из всех запрошенных партиций, и затем возвращает их. + # + # Returns Array Of Hash. + # + def data(group = {}, parts = {}) + parts = partitions(group, parts) + group = prepared_group(group) + prepared_parts(parts).flat_map do |partition| + rows = partition_data(group, partition) + block_given? ? yield(rows) : rows + end + end + + # Public: Возвращает массив партиций (подпартиций) группы в виде хешей. + # + # Если партиция не указана, возвращает все партиции группы. + # + # group - Hash - группа (опционально, если не используются группы). + # parts - Array of Hash - список партиций (опционально). + # По умолчанию, выбираются все данные группы. + # + # Returns Array Of Hash. + # + def partitions(group = {}, parts = {}) + partitions_raw(group, parts).map do |part| + # parse and exclude counter_name and group + part = part.split(key_delimiter, -1).from(1).from(group_keys.size) + # construct hash + Hash[partition_keys.zip(part)].with_indifferent_access + end + end + + # Public: Транзакционно удаляет все данные счетчика. + # + # group - Hash - группа (опционально, если не используются группы). + # + # Если передан блок, то вызывает блок, после удаления всех данных, в транзакции. + # + # Returns Nothing. + # + def delete_all!(group) + parts = partitions(group) + transaction do + delete_all_direct!(group, redis, parts) + yield if block_given? + end + end + + # Public: Транзакционно удаляет данные всех указанных партиций. + # + # parts - Array of Hash - список партиций. + # + # Если передан блок, то вызывает блок, после удаления всех данных, в транзакции. + # + # Returns Nothing. + # + def delete_partitions!(group, parts) + if parts.blank? + raise ArgumentError, 'You must specify a partitions' + end + + parts = Array.wrap(parts).flat_map { |part| partitions(group, part) } + + transaction do + parts.each { |partition| delete_partition_direct!(group, partition) } + yield if block_given? + end + end + + # Public: Нетранзакционно удаляет все данные счетчика. + # + # group - Hash - группа. + # write_session - Redis - соединение с Redis, в рамках которого + # будет производится удаление (опционально). + # По умолчанию - основное соединение счетчика. + # + # Returns Nothing. + # + def delete_all_direct!(group, write_session = redis, parts = partitions(group)) + parts.each { |partition| delete_partition_direct!(group, partition, write_session) } + end + + # Public: Нетранзакционно удаляет данные конкретной конечной партиции. + # + # group - Hash - группа. + # partition - Hash - партиция. + # write_session - Redis - соединение с Redis, в рамках которого + # будет производится удаление (опционально). + # По умолчанию - основное соединение счетчика. + # + # Returns Nothing. + # + def delete_partition_direct!(group, partition = {}, write_session = redis) + group = prepared_group(group) + partition = prepared_parts(partition, :only_leaf => true) + key = key(partition, group) + write_session.del(key) + end + protected - def key(partition = partition_params) - [counter_name, group_params, partition].flatten.compact.join(key_delimiter) + def key(partition = partition_params, group = group_params) + [counter_name, group, partition].flatten.compact.join(key_delimiter) end def group_params @@ -44,6 +151,88 @@ def partition_keys def group_keys @group_keys ||= Array.wrap(options.fetch(:group_keys, [])) end + + + # Protected: Возвращает группу в виде массива параметров, однозначно её идентифицирующих. + # + # group - Hash - группа. + # options - Hash - хеш опций: + # :only_leaf - Boolean - выбирать только листовые группы (по умолачнию - true). + # Если флаг установлен в true и передана не листовая группа, то + # будет сгенерировано исключение KeyError. + # + # Метод генерирует исключение ArgumentError, если переданы параметры не верно идентифицирующие группу. + # Например: ключи группировки счетчика {:param1, :param2, :param3}, а переданы {:param1, :param3}. + # Метод генерирует исключение ArgumentError, 'You must specify a group', + # если группа передана в виде пустого хеша, но группировка используется в счетчике. + # + # Returns Array. + # + def prepared_group(group, options = {}) + if group_keys.present? && group.blank? + raise ArgumentError, 'You must specify a group' + end + + default_options = {:only_leaf => true} + options.reverse_merge!(default_options) + + group = group.with_indifferent_access + group_keys.inject(Array.new) do |result, key| + param = (options[:only_leaf] ? group.fetch(key) : group[key]) + next result unless group.has_key?(key) + next result << param if result.size >= group_keys.index(key) + + raise ArgumentError, 'An incorrectly specified group %s' % [group] + end + end + + + # Protected: Возвращает массив партиций, где каждая партиция, + # представляет собой массив параметров, однозначно её идентифицирующих. + # + # parts - Array of Hash - список партиций. + # options - Hash - хеш опций: + # :only_leaf - Boolean - выбирать только листовые партиции (по умолачнию - false). + # Если флаг установлен в true и передана не листовая партиция, то + # будет сгенерировано исключение KeyError. + # + # Метод генерирует исключение ArgumentError, если переданы параметры не верно идентифицирующие партицию. + # Например: ключи партиционирования счетчика {:param1, :param2, :param3}, а переданы {:param1, :param3}. + # + # Returns Array of Array. + # + def prepared_parts(parts, options = {}) + default_options = {:only_leaf => false} + options.reverse_merge!(default_options) + + parts = Array.wrap(parts).map(&:with_indifferent_access) + parts.map do |partition| + partition_keys.inject(Array.new) do |result, key| + param = (options[:only_leaf] ? partition.fetch(key) : partition[key]) + next result unless partition.has_key?(key) + next result << param if result.size >= partition_keys.index(key) + + raise ArgumentError, 'An incorrectly specified partition %s' % [partition] + end + end + end + + # Protected: Возвращает данные партиции в виде массива хешей. + # + # Каждый элемент массива, представлен в виде хеша, содержащего все параметры уникального значения. + # + # group - Array - листовая группа - массив параметров однозначно идентифицирующий группу. + # partition - Array - листовая партиция - массив параметров однозначно идентифицирующий партицию. + # + # Returns Array of WithIndifferentAccess. + # + def partition_data(group, partition) + keys = value_keys + redis.smembers(key(partition, group)).inject(Array.new) do |result, (key, value)| + values = key.split(value_delimiter, -1) << value.to_i + result << Hash[keys.zip(values)].with_indifferent_access + end + end end end diff --git a/lib/redis_counters/unique_values_lists/fast.rb b/lib/redis_counters/unique_values_lists/fast.rb index 81eea49..7a3834c 100644 --- a/lib/redis_counters/unique_values_lists/fast.rb +++ b/lib/redis_counters/unique_values_lists/fast.rb @@ -4,15 +4,48 @@ module RedisCounters module UniqueValuesLists - # Список уникального значений, на основе не блокирующего алгоритма. + # Список уникальных значений, на основе не блокирующего алгоритма. # # Особенности: # * 2-х кратный расхзод памяти в случае использования партиций; # * Не ведет список партиций; # * Не транзакционен. + # * Методы delete_partitions! и delete_partition_direct!, удаляют только дублирующие партиции, + # но не удаляют данные из основной партиции. + # Для удаления основной партиции необходимо вызвать delete_main_partition!, + # либо воспользоваться методом delete_all! class Fast < UniqueValuesLists::Base + # Public: Нетранзакционно удаляет все данные счетчика. + # + # group - Hash - группа. + # write_session - Redis - соединение с Redis, в рамках которого + # будет производится удаление (опционально). + # По умолчанию - основное соединение счетчика. + # + # Returns Nothing. + # + def delete_all_direct!(group, write_session = redis, parts = partitions(group)) + super(group, write_session, parts) + delete_main_partition!(group, write_session) + end + + # Public: Удаляет основную партицию. + # + # group - Hash - группа. + # write_session - Redis - соединение с Redis, в рамках которого + # будет производится удаление (опционально). + # По умолчанию - основное соединение счетчика. + # + # Returns Nothing. + # + def delete_main_partition!(group, write_session = redis) + group = prepared_group(group) + key = key([], group) + write_session.del(key) + end + protected def process_value @@ -35,8 +68,25 @@ def current_partition_key key end - def partitions - redis.keys(key('*')) + # Protected: Возвращает массив листовых партиций в виде ключей. + # + # Если группа не указана и нет группировки в счетчике, то возвращает все партиции. + # Если партиция не указана, возвращает все партиции группы (все партиции, если нет группировки). + # + # group - Hash - группа. + # parts - Array of Hash - список партиций. + # + # Returns Array of Hash. + # + def partitions_raw(group = {}, parts = {}) + group = prepared_group(group) + prepared_parts(parts).inject(Array.new) do |result, partition| + strict_pattern = key(partition, group) if (group.present? && partition_keys.blank?) || partition.present? + fuzzy_pattern = key(partition << '*', group) + result |= redis.keys(strict_pattern) if strict_pattern.present? + result |= redis.keys(fuzzy_pattern) if fuzzy_pattern.present? + result + end end end diff --git a/lib/redis_counters/unique_values_lists/standard.rb b/lib/redis_counters/unique_values_lists/standard.rb index 452132e..be8b90f 100644 --- a/lib/redis_counters/unique_values_lists/standard.rb +++ b/lib/redis_counters/unique_values_lists/standard.rb @@ -4,7 +4,7 @@ module RedisCounters module UniqueValuesLists - # Список уникального значений, на основе механизма оптимистических блокировок. + # Список уникальных значений, на основе механизма оптимистических блокировок. # # смотри Optimistic locking using check-and-set: # http://redis.io/topics/transactions @@ -17,6 +17,28 @@ module UniqueValuesLists class Standard < Base PARTITIONS_LIST_POSTFIX = :partitions + # Public: Нетранзакционно удаляет данные конкретной конечной партиции. + # + # partition - Hash - партиция. + # write_session - Redis - соединение с Redis, в рамках которого + # будет производится удаление (опционально). + # По умолчанию - основное соединение счетчика. + # + # Если передан блок, то вызывает блок, после удаления всех данных, в транзакции. + # + # Returns Nothing. + # + def delete_partition_direct!(group, partition = {}, write_session = redis) + super(group, partition, write_session) + + # удаляем партицию из списка + return unless use_partitions? + group = prepared_group(group) + partition = prepared_parts(partition, :only_leaf => true) + partition = partition.flatten.join(key_delimiter) + write_session.lrem(partitions_list_key(group), 0, partition) + end + protected def process_value @@ -51,13 +73,13 @@ def watch_partitions_list end def watch_all_partitions - partitions.each do |partition| + all_partitions.each do |partition| redis.watch(key(partition)) end end def value_already_exists? - partitions.reverse.any? do |partition| + all_partitions.reverse.any? do |partition| redis.sismember(key(partition), value) end end @@ -66,11 +88,12 @@ def add_value redis.sadd(key, value) end - def partitions + def all_partitions(group = group_params) return @partitions unless @partitions.nil? return (@partitions = [nil]) unless use_partitions? - @partitions = redis.smembers(partitions_list_key).map do |partition| + @partitions = redis.lrange(partitions_list_key(group), 0, -1) + @partitions = @partitions.map do |partition| partition.split(key_delimiter) end .delete_if(&:empty?) @@ -79,11 +102,11 @@ def partitions def add_partition return unless use_partitions? return unless new_partition? - redis.sadd(partitions_list_key, current_partition) + redis.rpush(partitions_list_key, current_partition) end - def partitions_list_key - [counter_name, group_params, PARTITIONS_LIST_POSTFIX].flatten.join(key_delimiter) + def partitions_list_key(group = group_params) + [counter_name, group, PARTITIONS_LIST_POSTFIX].flatten.join(key_delimiter) end def current_partition @@ -91,7 +114,31 @@ def current_partition end def new_partition? - !partitions.include?(current_partition.split(key_delimiter)) + !all_partitions.include?(current_partition.split(key_delimiter)) + end + + + # Protected: Возвращает массив листовых партиций в виде ключей. + # + # Если группа не указана и нет группировки в счетчике, то возвращает все партиции. + # Если партиция не указана, возвращает все партиции группы (все партиции, если нет группировки). + # + # group - Hash - группа. + # parts - Array of Hash - список партиций. + # + # Returns Array of Hash. + # + def partitions_raw(group = {}, parts = {}) + reset_partitions_cache + group = prepared_group(group) + partitions_keys = all_partitions(group).map { |partition| key(partition, group) } + + prepared_parts(parts).flat_map do |partition| + strict_pattern = key(partition, group) if (group.present? && partition_keys.blank?) || partition.present? + fuzzy_pattern = key(partition << '', group) + partitions_keys.select { |part| part.eql?(strict_pattern) } | + partitions_keys.select { |part| part.start_with?(fuzzy_pattern) } + end.uniq end end diff --git a/spec/redis_counters/hash_counter_spec.rb b/spec/redis_counters/hash_counter_spec.rb index 5d97534..2030944 100644 --- a/spec/redis_counters/hash_counter_spec.rb +++ b/spec/redis_counters/hash_counter_spec.rb @@ -6,17 +6,6 @@ let(:options) { { :counter_name => :test_counter, :field_name => :test_field } } let(:counter) { described_class.new(redis, options) } - context 'when check interface' do - it { expect(counter).to respond_to :process } - it { expect(counter).to respond_to :increment } - it { expect(counter).to respond_to :partitions } - it { expect(counter).to respond_to :partitions_raw } - it { expect(counter).to respond_to :data } - it { expect(counter).to respond_to :delete_all! } - it { expect(counter).to respond_to :delete_partitions! } - it { expect(counter).to respond_to :delete_partition_direct! } - end - context 'when field_name and group_keys not given' do let(:options) { { :counter_name => :test_counter } } @@ -45,11 +34,14 @@ } } before { value.times { counter.process(:param1 => 11, :param2 => 22, :param3 => 33) } } + before { value.times { counter.process(:param1 => 11, :param2 => nil, :param3 => 33) } } it { expect(redis.keys('*')).to have(1).key } it { expect(redis.keys('*').first).to eq 'test_counter' } it { expect(redis.hexists('test_counter', '11:22')).to be_true } it { expect(redis.hget('test_counter', '11:22')).to eq value.to_s } + it { expect(redis.hexists('test_counter', '11:')).to be_true } + it { expect(redis.hget('test_counter', '11:')).to eq value.to_s } end context 'when exists group_keys given' do @@ -61,6 +53,8 @@ before { value.times { counter.process(:param1 => 11, :param2 => 22, :param3 => 33) } } before { 2.times { counter.process(:param1 => 12, :param2 => 22, :param3 => 33) } } + before { 1.times { counter.process(:param1 => 12, :param2 => nil, :param3 => 33) } } + before { 1.times { counter.process(:param1 => 12, :param2 => '', :param3 => 33) } } it { expect(redis.keys('*')).to have(1).key } it { expect(redis.keys('*').first).to eq 'test_counter' } @@ -68,6 +62,8 @@ it { expect(redis.hget('test_counter', '11:22')).to eq value.to_s } it { expect(redis.hexists('test_counter', '12:22')).to be_true } it { expect(redis.hget('test_counter', '12:22')).to eq 2.to_s } + it { expect(redis.hexists('test_counter', '12:')).to be_true } + it { expect(redis.hget('test_counter', '12:')).to eq 2.to_s } end context 'when not exists group_keys given' do @@ -120,6 +116,30 @@ it { expect(redis.hget('test_counter:false', 'test_field')).to eq 3.to_s } end + context 'when one partition_key is nil or empty string' do + let(:options) { { + :counter_name => :test_counter, + :field_name => :test_field, + :partition_keys => [:param1, :param2] + } } + + before { value.times { counter.process(:param1 => 11, :param2 => 22, :param3 => 33) } } + before { 3.times { counter.process(:param1 => 21, :param2 => 22, :param3 => 33) } } + before { 1.times { counter.process(:param1 => 21, :param2 => nil, :param3 => 33) } } + before { 1.times { counter.process(:param1 => 21, :param2 => '', :param3 => 33) } } + + it { expect(redis.keys('*')).to have(3).key } + it { expect(redis.keys('*').first).to eq 'test_counter:11:22' } + it { expect(redis.keys('*').second).to eq 'test_counter:21:22' } + it { expect(redis.keys('*').third).to eq 'test_counter:21:' } + it { expect(redis.hexists('test_counter:11:22', 'test_field')).to be_true } + it { expect(redis.hget('test_counter:11:22', 'test_field')).to eq value.to_s } + it { expect(redis.hexists('test_counter:21:22', 'test_field')).to be_true } + it { expect(redis.hget('test_counter:21:22', 'test_field')).to eq 3.to_s } + it { expect(redis.hexists('test_counter:21:', 'test_field')).to be_true } + it { expect(redis.hget('test_counter:21:', 'test_field')).to eq 2.to_s } + end + context 'when partition_keys consists of mixed types' do let(:options) { { :counter_name => :test_counter, @@ -213,10 +233,37 @@ before { value.times { counter.process(:param1 => 11, :param2 => 22, :param3 => 33) } } before { 3.times { counter.process(:param1 => 21, :param2 => 22, :param3 => 33) } } before { 2.times { counter.process(:param1 => 21, :param2 => 23, :param3 => 31) } } + before { 1.times { counter.process(:param1 => 21, :param2 => nil, :param3 => 31) } } + before { 4.times { counter.process(:param1 => 21, :param2 => '', :param3 => 31) } } - it { expect(counter.data(partitions)).to have(2).row } + it { expect(counter.data(partitions)).to have(3).row } it { expect(counter.data(partitions).first[:value]).to eq 3 } it { expect(counter.data(partitions).second[:value]).to eq 2 } + it { expect(counter.data(partitions).third[:value]).to eq 5 } + end + + context 'when group_keys and one group key is nil' do + let(:options) { { + :counter_name => :test_counter, + :field_name => :test_field, + :partition_keys => [:param1, :param2], + :group_keys => [:param3, :param4] + } } + + let(:partitions) { {:param1 => 21, 'param3' => 33, :param2 => nil} } + + before { 1.times { counter.process(:param1 => 21, :param2 => nil, :param3 => 31, :param4 => 1) } } + before { 4.times { counter.process(:param1 => 21, :param2 => '', :param3 => 31, :param4 => nil) } } + before { 1.times { counter.process(:param1 => 21, :param2 => nil, :param3 => 31, :param4 => '') } } + before { 3.times { counter.process(:param1 => 21, :param2 => '', :param3 => 31, :param4 => 1) } } + + it { expect(counter.data(partitions)).to have(2).row } + it { expect(counter.data(partitions).first[:value]).to eq 4 } + it { expect(counter.data(partitions).first[:param3]).to eq '31' } + it { expect(counter.data(partitions).first[:param4]).to eq '1' } + it { expect(counter.data(partitions).second[:value]).to eq 5 } + it { expect(counter.data(partitions).second[:param3]).to eq '31' } + it { expect(counter.data(partitions).second[:param4]).to eq '' } end context 'when group_keys given' do @@ -230,6 +277,8 @@ before { value.times { counter.process(:param1 => 11, :param2 => 22, :param3 => 33) } } before { 3.times { counter.process(:param1 => 21, :param2 => 22, :param3 => 33) } } before { 2.times { counter.process(:param1 => 21, :param2 => 22, :param3 => 31) } } + before { 1.times { counter.process(:param1 => 21, :param2 => nil, :param3 => 31) } } + before { 4.times { counter.process(:param1 => 21, :param2 => '', :param3 => 31) } } context 'when partition as Hash_given' do let(:partitions) { {:param1 => 21, 'param3' => 33, :param2 => 22} } @@ -246,6 +295,22 @@ it { expect(counter.data(partitions).second[:param3]).to eq '31' } end + context 'when partition param is empty string' do + let(:partitions) { {:param1 => 21, 'param3' => 33, :param2 => ''} } + + it { expect(counter.data(partitions)).to have(1).row } + it { expect(counter.data(partitions).first[:value]).to eq 5 } + it { expect(counter.data(partitions).first[:param3]).to eq '31' } + end + + context 'when partition param is empty nil' do + let(:partitions) { {:param1 => 21, 'param3' => 33, :param2 => nil} } + + it { expect(counter.data(partitions)).to have(1).row } + it { expect(counter.data(partitions).first[:value]).to eq 5 } + it { expect(counter.data(partitions).first[:param3]).to eq '31' } + end + context 'when few partition_given' do let(:partitions) do [ diff --git a/spec/redis_counters/unique_hash_counter_spec.rb b/spec/redis_counters/unique_hash_counter_spec.rb index 232c0f8..cd2bcbd 100644 --- a/spec/redis_counters/unique_hash_counter_spec.rb +++ b/spec/redis_counters/unique_hash_counter_spec.rb @@ -9,8 +9,28 @@ :field_name => :test_field, :unique_list => { :list_class => RedisCounters::UniqueValuesLists::Standard } } } + let(:counter) { described_class.new(redis, options) } + context '#partitions' do + let(:options) { { + :counter_name => :visits_by_day, + :field_name => 'dd', + :partition_keys => [:date], + :unique_list => { + :list_class => RedisCounters::UniqueValuesLists::Fast, + :value_keys => [:sid], + :group_keys => [:p1], + :partition_keys => [:p2] + } + } } + + before { counter.process(:date => '2013-10-17', :p1 => '2', :p2 => '3', :sid => '1') } + + it { expect(counter.partitions).to have(1).partitions } + it { expect(counter.partitions.first).to include ({:date => '2013-10-17'}) } + end + it { expect(counter).to be_a_kind_of RedisCounters::HashCounter } context 'when unique_list not given' do @@ -54,10 +74,10 @@ it { expect(redis.hget('test_counter:2013-04-27', '2')).to eq 2.to_s } it { expect(redis.hget('test_counter:2013-04-28', '2')).to eq 2.to_s } - it { expect(redis.smembers('test_counter:uq:1:partitions')).to eq ['2013-04-27', '2013-04-28'] } - it { expect(redis.smembers('test_counter:uq:2:partitions')).to eq ['2013-04-27'] } - it { expect(redis.smembers('test_counter:uq:1:2013-04-27')).to eq ['4'] } - it { expect(redis.smembers('test_counter:uq:2:2013-04-27')).to eq ['3', '2', '1'] } - it { expect(redis.smembers('test_counter:uq:1:2013-04-28')).to eq ['5', '1'] } + it { expect(redis.lrange('test_counter_uq:1:partitions', 0, -1)).to eq ['2013-04-28', '2013-04-27'] } + it { expect(redis.lrange('test_counter_uq:2:partitions', 0, -1)).to eq ['2013-04-27'] } + it { expect(redis.smembers('test_counter_uq:1:2013-04-27')).to eq ['4'] } + it { expect(redis.smembers('test_counter_uq:2:2013-04-27')).to eq ['3', '2', '1'] } + it { expect(redis.smembers('test_counter_uq:1:2013-04-28')).to eq ['5', '1'] } end end \ No newline at end of file diff --git a/spec/redis_counters/unique_values_lists/standard_spec.rb b/spec/redis_counters/unique_values_lists/standard_spec.rb index 77e492d..6664f85 100644 --- a/spec/redis_counters/unique_values_lists/standard_spec.rb +++ b/spec/redis_counters/unique_values_lists/standard_spec.rb @@ -26,15 +26,15 @@ it { expect(redis.keys('*')).to have(5).key } context 'when check partitions' do - it { expect(redis.exists("test_counter:group1:#{partitions_list_postfix}")).to be_true } - it { expect(redis.exists("test_counter:group2:#{partitions_list_postfix}")).to be_true } + it { expect(redis.lrange("test_counter:group1:#{partitions_list_postfix}", 0, -1)).to be_true } + it { expect(redis.lrange("test_counter:group2:#{partitions_list_postfix}", 0, -1)).to be_true } - it { expect(redis.smembers("test_counter:group1:#{partitions_list_postfix}")).to have(2).keys } - it { expect(redis.smembers("test_counter:group2:#{partitions_list_postfix}")).to have(1).keys } + it { expect(redis.lrange("test_counter:group1:#{partitions_list_postfix}", 0, -1)).to have(2).keys } + it { expect(redis.lrange("test_counter:group2:#{partitions_list_postfix}", 0, -1)).to have(1).keys } - it { expect(redis.smembers("test_counter:group1:#{partitions_list_postfix}")).to include 'part1:part2' } - it { expect(redis.smembers("test_counter:group1:#{partitions_list_postfix}")).to include 'part2:part2' } - it { expect(redis.smembers("test_counter:group2:#{partitions_list_postfix}")).to include 'part1:part2' } + it { expect(redis.lrange("test_counter:group1:#{partitions_list_postfix}", 0, -1)).to include 'part1:part2' } + it { expect(redis.lrange("test_counter:group1:#{partitions_list_postfix}", 0, -1)).to include 'part2:part2' } + it { expect(redis.lrange("test_counter:group2:#{partitions_list_postfix}", 0, -1)).to include 'part1:part2' } end context 'when check values' do @@ -70,10 +70,10 @@ context 'when check partitions' do it { expect(redis.exists("test_counter:#{partitions_list_postfix}")).to be_true } - it { expect(redis.smembers("test_counter:#{partitions_list_postfix}")).to have(2).keys } + it { expect(redis.lrange("test_counter:#{partitions_list_postfix}", 0, -1)).to have(2).keys } - it { expect(redis.smembers("test_counter:#{partitions_list_postfix}")).to include 'part1:part2' } - it { expect(redis.smembers("test_counter:#{partitions_list_postfix}")).to include 'part2:part2' } + it { expect(redis.lrange("test_counter:#{partitions_list_postfix}", 0, -1)).to include 'part1:part2' } + it { expect(redis.lrange("test_counter:#{partitions_list_postfix}", 0, -1)).to include 'part2:part2' } end context 'when check values' do diff --git a/spec/support/unique_values_lists.rb b/spec/support/unique_values_lists.rb index d7923a5..cfea0c9 100644 --- a/spec/support/unique_values_lists.rb +++ b/spec/support/unique_values_lists.rb @@ -1,62 +1,66 @@ +# coding: utf-8 shared_examples_for 'unique_values_lists' do let(:redis) { MockRedis.new } let(:values) { rand(10) + 1 } + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0] + } } + let(:counter) { described_class.new(redis, options) } - context 'when value_keys not given' do - let(:options) { { - :counter_name => :test_counter - } } + context '#add' do + context 'when value_keys not given' do + let(:options) { {:counter_name => :test_counter} } - it { expect { counter.add }.to raise_error KeyError } - end + it { expect { counter.add }.to raise_error KeyError } + end - context 'when unknown value_key given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1] - } } + context 'when unknown value_key given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1] + } } - it { expect { counter.add(:param1 => 1) }.to raise_error KeyError } - end + it { expect { counter.add(:param1 => 1) }.to raise_error KeyError } + end - context 'when unknown group_key given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0], - :group_keys => [:param1, :param2], - } } + context 'when unknown group_key given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0], + :group_keys => [:param1, :param2], + } } - it { expect { counter.add(:param0 => 1, :param1 => 2) }.to raise_error KeyError } - end + it { expect { counter.add(:param0 => 1, :param1 => 2) }.to raise_error KeyError } + end - context 'when unknown partition_key given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0], - :partition_keys => [:param1, :param2], - } } + context 'when unknown partition_key given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0], + :partition_keys => [:param1, :param2], + } } - it { expect { counter.add(:param0 => 1, :param1 => 2) }.to raise_error KeyError } - end + it { expect { counter.add(:param0 => 1, :param1 => 2) }.to raise_error KeyError } + end - context 'when group and partition keys given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1], - :group_keys => [:param2], - :partition_keys => [:param3, :param4] - } } + context 'when group and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:param2], + :partition_keys => [:param3, :param4] + } } - before { values.times { counter.process(:param0 => 1, :param1 => 2, :param2 => :group1, :param3 => :part1, :param4 => :part2) } } - before { values.times { counter.process(:param0 => 2, :param1 => 1, :param2 => :group1, :param3 => :part1, :param4 => :part2) } } - before { values.times { counter.process(:param0 => 3, :param1 => 2, :param2 => :group1, :param3 => :part2, :param4 => :part2) } } - before { values.times { counter.process(:param0 => 4, :param1 => 5, :param2 => :group2, :param3 => :part1, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 1, :param1 => 2, :param2 => :group1, :param3 => :part1, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 2, :param1 => 1, :param2 => :group1, :param3 => :part1, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 3, :param1 => 2, :param2 => :group1, :param3 => :part2, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 4, :param1 => 5, :param2 => :group2, :param3 => :part1, :param4 => :part2) } } - it { expect(redis.keys('*')).to have(5).key } + it { expect(redis.keys('*')).to have(5).key } - context 'when check values' do it { expect(redis.exists("test_counter:group1:part1:part2")).to be_true } it { expect(redis.exists("test_counter:group1:part2:part2")).to be_true } it { expect(redis.exists("test_counter:group2:part1:part2")).to be_true } @@ -70,22 +74,20 @@ it { expect(redis.smembers("test_counter:group1:part2:part2")).to include '3:2' } it { expect(redis.smembers("test_counter:group2:part1:part2")).to include '4:5' } end - end - context 'when group and partition keys no given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1] - } } + context 'when group and partition keys no given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1] + } } - before { values.times { counter.process(:param0 => 1, :param1 => 2) } } - before { values.times { counter.process(:param0 => 1, :param1 => 2) } } - before { values.times { counter.process(:param0 => 2, :param1 => 1) } } - before { values.times { counter.process(:param0 => 3, :param1 => 2) } } + before { values.times { counter.add(:param0 => 1, :param1 => 2) } } + before { values.times { counter.add(:param0 => 1, :param1 => 2) } } + before { values.times { counter.add(:param0 => 2, :param1 => 1) } } + before { values.times { counter.add(:param0 => 3, :param1 => 2) } } - it { expect(redis.keys('*')).to have(1).key } + it { expect(redis.keys('*')).to have(1).key } - context 'when check values' do it { expect(redis.exists("test_counter")).to be_true } it { expect(redis.smembers("test_counter")).to have(3).keys } @@ -93,23 +95,21 @@ it { expect(redis.smembers("test_counter")).to include '2:1' } it { expect(redis.smembers("test_counter")).to include '3:2' } end - end - context 'when no group keys given, but partition keys given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1], - :partition_keys => [:param3, :param4] - } } + context 'when no group keys given, but partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :partition_keys => [:param3, :param4] + } } - before { values.times { counter.process(:param0 => 1, :param1 => 2, :param3 => :part1, :param4 => :part2) } } - before { values.times { counter.process(:param0 => 2, :param1 => 1, :param3 => :part1, :param4 => :part2) } } - before { values.times { counter.process(:param0 => 3, :param1 => 2, :param3 => :part2, :param4 => :part2) } } - before { values.times { counter.process(:param0 => 4, :param1 => 5, :param3 => :part1, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 1, :param1 => 2, :param3 => :part1, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 2, :param1 => 1, :param3 => :part1, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 3, :param1 => 2, :param3 => :part2, :param4 => :part2) } } + before { values.times { counter.add(:param0 => 4, :param1 => 5, :param3 => :part1, :param4 => :part2) } } - it { expect(redis.keys('*')).to have(3).key } + it { expect(redis.keys('*')).to have(3).key } - context 'when check values' do it { expect(redis.exists("test_counter:part1:part2")).to be_true } it { expect(redis.exists("test_counter:part2:part2")).to be_true } @@ -121,23 +121,21 @@ it { expect(redis.smembers("test_counter:part2:part2")).to include '3:2' } it { expect(redis.smembers("test_counter:part1:part2")).to include '4:5' } end - end - context 'when group keys given, but partition keys not given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1], - :group_keys => [:param2] - } } + context 'when group keys given, but partition keys not given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:param2] + } } - before { values.times { counter.process(:param0 => 1, :param1 => 2, :param2 => :group1) } } - before { values.times { counter.process(:param0 => 2, :param1 => 1, :param2 => :group1) } } - before { values.times { counter.process(:param0 => 3, :param1 => 2, :param2 => :group1) } } - before { values.times { counter.process(:param0 => 4, :param1 => 5, :param2 => :group2) } } + before { values.times { counter.add(:param0 => 1, :param1 => 2, :param2 => :group1) } } + before { values.times { counter.add(:param0 => 2, :param1 => 1, :param2 => :group1) } } + before { values.times { counter.add(:param0 => 3, :param1 => 2, :param2 => :group1) } } + before { values.times { counter.add(:param0 => 4, :param1 => 5, :param2 => :group2) } } - it { expect(redis.keys('*')).to have(2).key } + it { expect(redis.keys('*')).to have(2).key } - context 'when check values' do it { expect(redis.exists("test_counter:group1")).to be_true } it { expect(redis.exists("test_counter:group2")).to be_true } @@ -149,24 +147,440 @@ it { expect(redis.smembers("test_counter:group1")).to include '3:2' } it { expect(redis.smembers("test_counter:group2")).to include '4:5' } end + + context 'when block given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0] + } } + + context 'when item added' do + it { expect { |b| counter.add(:param0 => 1, &b) }.to yield_with_args(redis) } + it { expect(counter.add(:param0 => 1)).to be_true } + end + + context 'when item not added' do + before { counter.add(:param0 => 1) } + + it { expect { |b| counter.add(:param0 => 1, &b) }.to_not yield_with_args(redis) } + it { expect(counter.add(:param0 => 1)).to be_false } + end + end end - context 'when block given' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0] - } } + context '#partitions' do + let(:group1_subgroup1) { {:group => :group1, :subgroup => :subgroup1} } + let(:group1_subgroup2) { {:group => :group1, :subgroup => :subgroup2} } + let(:group1_subgroup3) { {:group => :group1, :subgroup => :subgroup3} } + let(:group2_subgroup1) { {:group => :group2, :subgroup => :subgroup1} } + + let(:part1_subpart1) { {:part => 'part1', :subpart => 'subpart1'}.with_indifferent_access } + let(:part1_subpart2) { {:part => 'part1', :subpart => 'subpart2'}.with_indifferent_access } + let(:part2_subpart1) { {'part' => 'part2', :subpart => 'subpart1'}.with_indifferent_access } + + context 'when group and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:group, :subgroup], + :partition_keys => [:part, :subpart] + } } + + # 2 разных знач в одной группе и партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # дубль знач в другой партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart3) } } + # дубль знач в другой группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup3, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подпартиции + before { values.times { counter.add(:param0 => 3, :param1 => 4, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart2) } } + # новое значение в новой партиции + before { values.times { counter.add(:param0 => 4, :param1 => 5, :group => :group1, :subgroup => :subgroup1, :part => :part2, :subpart => :subpart1) } } + # новое значение в новой группе + before { values.times { counter.add(:param0 => 5, :param1 => 6, :group => :group2, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подгруппе + before { values.times { counter.add(:param0 => 6, :param1 => 7, :group => :group1, :subgroup => :subgroup2, :part => :part1, :subpart => :subpart1) } } + + context 'when no group given' do + it { expect { counter.partitions }.to raise_error ArgumentError } + end + + context 'when no leaf group given' do + it { expect { counter.partitions(:group => :group1) }.to raise_error KeyError } + end + + context 'when unknown group given' do + it { expect(counter.partitions({:group => :unknown_group, :subgroup => :subgroup})).to have(0).partitions } + end + + context 'when no partition given' do + it { expect(counter.partitions(group1_subgroup1)).to have(3).partitions } + it { expect(counter.partitions(group1_subgroup1).first).to eq part1_subpart1 } + it { expect(counter.partitions(group1_subgroup1).second).to eq part1_subpart2 } + it { expect(counter.partitions(group1_subgroup1).third).to eq part2_subpart1 } + # + it { expect(counter.partitions(group2_subgroup1)).to have(1).partitions } + it { expect(counter.partitions(group2_subgroup1).first).to eq part1_subpart1 } + end + + context 'when not leaf partition given' do + it { expect(counter.partitions(group1_subgroup1, [{:part => :part1}, {:part => :part1}, {:part => :part13}])).to have(2).partitions } + it { expect(counter.partitions(group1_subgroup1, {:part => :part1}).first).to eq part1_subpart1 } + it { expect(counter.partitions(group1_subgroup1, {:part => :part1}).second).to eq part1_subpart2 } + end + + context 'when leaf partition given' do + it { expect(counter.partitions(group1_subgroup1, {:part => :part1, 'subpart' => 'subpart1'})).to have(1).partitions } + it { expect(counter.partitions(group1_subgroup1, {:part => :part1, 'subpart' => 'subpart1'}).first).to eq part1_subpart1 } + end + end + + context 'when not group keys given and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :partition_keys => [:part, :subpart] + } } + + # 2 разных знач в одной партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :part => :part1, :subpart => :subpart1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :part => :part1, :subpart => :subpart1) } } + # дубль знач в другой партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :part => :part1, :subpart => :subpart3) } } + # новое значение в новой подпартиции + before { values.times { counter.add(:param0 => 3, :param1 => 4, :part => :part1, :subpart => :subpart2) } } + # новое значение в новой партиции + before { values.times { counter.add(:param0 => 4, :param1 => 5, :part => :part2, :subpart => :subpart1) } } + + context 'when no group given' do + it { expect { counter.partitions }.to_not raise_error KeyError } + end + + context 'when no partition given' do + it { expect(counter.partitions).to have(3).partitions } + it { expect(counter.partitions.first).to eq part1_subpart1 } + it { expect(counter.partitions.second).to eq part1_subpart2 } + it { expect(counter.partitions.third).to eq part2_subpart1 } + end + + context 'when not leaf partition given' do + it { expect(counter.partitions({}, [{:part => :part1}, {:part => :part1}, {:part => :part13}])).to have(2).partitions } + it { expect(counter.partitions({}, {:part => :part1}).first).to eq part1_subpart1 } + it { expect(counter.partitions({}, {:part => :part1}).second).to eq part1_subpart2 } + end + + context 'when leaf partition given' do + it { expect(counter.partitions({}, {:part => :part1, 'subpart' => 'subpart1'})).to have(1).partitions } + it { expect(counter.partitions({}, {:part => :part1, 'subpart' => 'subpart1'}).first).to eq part1_subpart1 } + end + end + + context 'when group keys given and partition keys not given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:group, :subgroup] + } } + + # 2 разных знач в одной группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :group => :group1, :subgroup => :subgroup1) } } + # дубль знач в другой группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup3) } } + # новое значение в новой группе + before { values.times { counter.add(:param0 => 5, :param1 => 6, :group => :group2, :subgroup => :subgroup1) } } + # новое значение в новой подгруппе + before { values.times { counter.add(:param0 => 6, :param1 => 7, :group => :group1, :subgroup => :subgroup2) } } + + + context 'when no partition given' do + it { expect(counter.partitions(group1_subgroup1)).to have(1).partitions } + it { expect(counter.partitions(group1_subgroup1).first).to eq Hash.new } + + it { expect(counter.partitions(group2_subgroup1)).to have(1).partitions } + it { expect(counter.partitions(group2_subgroup1).first).to eq Hash.new } + end + end + end - context 'when item added' do - it { expect { |b| counter.process(:param0 => 1, &b) }.to yield_with_args(redis) } - it { expect(counter.process(:param0 => 1)).to be_true } + context '#data' do + let(:group1_subgroup1) { {:group => :group1, :subgroup => :subgroup1} } + let(:group1_subgroup2) { {:group => :group1, :subgroup => :subgroup2} } + let(:group1_subgroup3) { {:group => :group1, :subgroup => :subgroup3} } + let(:group2_subgroup1) { {:group => :group2, :subgroup => :subgroup1} } + + let(:part1_subpart1) { {:part => 'part1', :subpart => 'subpart1'}.with_indifferent_access } + let(:part1_subpart2) { {:part => 'part1', :subpart => 'subpart2'}.with_indifferent_access } + let(:part2_subpart1) { {'part' => 'part2', :subpart => 'subpart1'}.with_indifferent_access } + + context 'when group and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:group, :subgroup], + :partition_keys => [:part, :subpart] + } } + + # 2 разных знач в одной группе и партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # дубль знач в другой партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart3) } } + # дубль знач в другой группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup3, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подпартиции + before { values.times { counter.add(:param0 => 3, :param1 => 4, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart2) } } + # новое значение в новой партиции + before { values.times { counter.add(:param0 => 4, :param1 => 5, :group => :group1, :subgroup => :subgroup1, :part => :part2, :subpart => :subpart1) } } + # новое значение в новой группе + before { values.times { counter.add(:param0 => 5, :param1 => 6, :group => :group2, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подгруппе + before { values.times { counter.add(:param0 => 6, :param1 => 7, :group => :group1, :subgroup => :subgroup2, :part => :part1, :subpart => :subpart1) } } + + context 'when no group given' do + it { expect { counter.data }.to raise_error ArgumentError } + end + + context 'when no leaf group given' do + it { expect { counter.data(:group => :group1) }.to raise_error KeyError } + end + + context 'when unknown group given' do + it { expect(counter.data({:group => :unknown_group, :subgroup => :subgroup})).to have(0).partitions } + end + + context 'when no partition given' do + it { expect(counter.data(group1_subgroup1)).to have(4).rows } + it { expect(counter.data(group1_subgroup1)).to include ({'param0' => '1', 'param1' => '2'}) } + it { expect(counter.data(group1_subgroup1)).to include ({'param0' => '1', 'param1' => '3'}) } + it { expect(counter.data(group1_subgroup1)).to include ({'param0' => '3', 'param1' => '4'}) } + it { expect(counter.data(group1_subgroup1)).to include ({'param0' => '4', 'param1' => '5'}) } + + it { expect(counter.data(group2_subgroup1)).to have(1).rows } + it { expect(counter.data(group2_subgroup1).first).to include ({'param0' => '5', 'param1' => '6'}) } + end + + context 'when not leaf partition given' do + it { expect(counter.data(group1_subgroup1, [{:part => :part1}, {:part => :part1}, {:part => :part13}])).to have(3).rows } + it { expect(counter.data(group1_subgroup1, {:part => :part1})).to include ({'param0' => '1', 'param1' => '2'}) } + it { expect(counter.data(group1_subgroup1, {:part => :part1})).to include ({'param0' => '1', 'param1' => '3'}) } + it { expect(counter.data(group1_subgroup1, {:part => :part1})).to include ({'param0' => '3', 'param1' => '4'}) } + end + + context 'when leaf partition given' do + it { expect(counter.data(group1_subgroup1, {:part => :part1, 'subpart' => 'subpart1'})).to have(2).rows } + it { expect(counter.data(group1_subgroup1, {:part => :part1, 'subpart' => 'subpart1'})).to include ({'param0' => '1', 'param1' => '2'}) } + it { expect(counter.data(group1_subgroup1, {:part => :part1, 'subpart' => 'subpart1'})).to include ({'param0' => '1', 'param1' => '3'}) } + end end - context 'when item not added' do - before { counter.process(:param0 => 1) } + context 'when not group keys given and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :partition_keys => [:part, :subpart] + } } + + # 2 разных знач в одной партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :part => :part1, :subpart => :subpart1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :part => :part1, :subpart => :subpart1) } } + # дубль знач в другой партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :part => :part1, :subpart => :subpart3) } } + # новое значение в новой подпартиции + before { values.times { counter.add(:param0 => 3, :param1 => 4, :part => :part1, :subpart => :subpart2) } } + # новое значение в новой партиции + before { values.times { counter.add(:param0 => 4, :param1 => 5, :part => :part2, :subpart => :subpart1) } } + + context 'when no group given' do + it { expect { counter.data }.to_not raise_error KeyError } + end + + context 'when no partition given' do + it { expect(counter.data).to have(4).rows } + it { expect(counter.data).to include ({'param0' => '1', 'param1' => '2'}) } + it { expect(counter.data).to include ({'param0' => '1', 'param1' => '3'}) } + it { expect(counter.data).to include ({'param0' => '3', 'param1' => '4'}) } + it { expect(counter.data).to include ({'param0' => '4', 'param1' => '5'}) } + end + + context 'when not leaf partition given' do + it { expect(counter.data({}, [{:part => :part1}, {:part => :part1}, {:part => :part13}])).to have(3).rows } + it { expect(counter.data({}, {:part => :part1})).to include ({'param0' => '1', 'param1' => '2'}) } + it { expect(counter.data({}, {:part => :part1})).to include ({'param0' => '1', 'param1' => '3'}) } + it { expect(counter.data({}, {:part => :part1})).to include ({'param0' => '3', 'param1' => '4'}) } + end + + context 'when leaf partition given' do + it { expect(counter.data({}, {:part => :part1, 'subpart' => 'subpart1'})).to include ({'param0' => '1', 'param1' => '2'}) } + it { expect(counter.data({}, {:part => :part1, 'subpart' => 'subpart1'}).first).to include ({'param0' => '1', 'param1' => '3'}) } + end + end + + context 'when group keys given and partition keys not given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:group, :subgroup] + } } + + # 2 разных знач в одной группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :group => :group1, :subgroup => :subgroup1) } } + # дубль знач в другой группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup3) } } + # новое значение в новой группе + before { values.times { counter.add(:param0 => 5, :param1 => 6, :group => :group2, :subgroup => :subgroup1) } } + # новое значение в новой подгруппе + before { values.times { counter.add(:param0 => 6, :param1 => 7, :group => :group1, :subgroup => :subgroup2) } } + + + context 'when no partition given' do + it { expect(counter.partitions(group1_subgroup1)).to have(1).partitions } + it { expect(counter.partitions(group1_subgroup1).first).to eq Hash.new } + + it { expect(counter.partitions(group2_subgroup1)).to have(1).partitions } + it { expect(counter.partitions(group2_subgroup1).first).to eq Hash.new } + end + end + end + + context '#delete_partitions!' do + let(:group1_subgroup1) { {:group => :group1, :subgroup => :subgroup1} } + let(:group1_subgroup2) { {:group => :group1, :subgroup => :subgroup2} } + let(:group1_subgroup3) { {:group => :group1, :subgroup => :subgroup3} } + let(:group2_subgroup1) { {:group => :group2, :subgroup => :subgroup1} } + + let(:part1_subpart1) { {:part => 'part1', :subpart => 'subpart1'}.with_indifferent_access } + let(:part1_subpart2) { {:part => 'part1', :subpart => 'subpart2'}.with_indifferent_access } + let(:part2_subpart1) { {'part' => 'part2', :subpart => 'subpart1'}.with_indifferent_access } + + context 'when group and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:group, :subgroup], + :partition_keys => [:part, :subpart] + } } + + # 2 разных знач в одной группе и партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # дубль знач в другой партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart3) } } + # дубль знач в другой группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup3, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подпартиции + before { values.times { counter.add(:param0 => 3, :param1 => 4, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart2) } } + # новое значение в новой партиции + before { values.times { counter.add(:param0 => 4, :param1 => 5, :group => :group1, :subgroup => :subgroup1, :part => :part2, :subpart => :subpart1) } } + # новое значение в новой группе + before { values.times { counter.add(:param0 => 5, :param1 => 6, :group => :group2, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подгруппе + before { values.times { counter.add(:param0 => 6, :param1 => 7, :group => :group1, :subgroup => :subgroup2, :part => :part1, :subpart => :subpart1) } } + + context 'when no group given' do + it { expect { counter.delete_partitions! }.to raise_error ArgumentError } + end + + context 'when no leaf group given' do + it { expect { counter.delete_partitions!({:group => :group1}, {:part => 1}) }.to raise_error KeyError } + end + + context 'when unknown group given' do + it { expect(counter.delete_partitions!({:group => :unknown_group, :subgroup => :subgroup}, {:part => 1})).to_not raise_error } + end + + context 'when no partition given' do + it { expect { counter.delete_partitions!({:group => :group1, :subgroup => :subgroup1}, {}) }.to raise_error ArgumentError } + end + + context 'when not leaf partition given' do + before { counter.delete_partitions!(group1_subgroup1, [{:part => :part1}, {:part => :part1}, {:part => :part13}]) } + + it { expect(counter.partitions(group1_subgroup1, :part => :part1)).to have(0).rows } + + it { expect(counter.partitions(group1_subgroup1, :part => :part2)).to be_present } + it { expect(counter.data(group1_subgroup1, :part => :part2)).to have(1).rows } + it { expect(counter.data(group1_subgroup1, :part => :part2)).to include ({'param0' => '4', 'param1' => '5'}) } + + it { expect(counter.partitions(group2_subgroup1, :part => :part1)).to be_present } + it { expect(counter.data(group2_subgroup1, :part => :part1)).to have(1).rows } + it { expect(counter.data(group2_subgroup1, :part => :part1)).to include ({'param0' => '5', 'param1' => '6'}) } + end + + context 'when leaf partition given' do + before { counter.delete_partitions!(group1_subgroup1, {:part => :part1, :subpart => :subpart1}) } + + it { expect(counter.partitions(group1_subgroup1, {:part => :part1, :subpart => :subpart1})).to have(0).rows } + + it { expect(counter.partitions(group1_subgroup1, {:part => :part1, :subpart => :subpart2})).to have(1).rows } + it { expect(counter.data(group1_subgroup1, {:part => :part1, :subpart => :subpart2})).to include ({'param0' => '3', 'param1' => '4'}) } + end + end + end - it { expect { |b| counter.process(:param0 => 1, &b) }.to_not yield_with_args(redis) } - it { expect(counter.process(:param0 => 1)).to be_false } + context '#delete_all!' do + let(:group1_subgroup1) { {:group => :group1, :subgroup => :subgroup1} } + let(:group1_subgroup2) { {:group => :group1, :subgroup => :subgroup2} } + let(:group1_subgroup3) { {:group => :group1, :subgroup => :subgroup3} } + let(:group2_subgroup1) { {:group => :group2, :subgroup => :subgroup1} } + + let(:part1_subpart1) { {:part => 'part1', :subpart => 'subpart1'}.with_indifferent_access } + let(:part1_subpart2) { {:part => 'part1', :subpart => 'subpart2'}.with_indifferent_access } + let(:part2_subpart1) { {'part' => 'part2', :subpart => 'subpart1'}.with_indifferent_access } + + context 'when group and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :group_keys => [:group, :subgroup], + :partition_keys => [:part, :subpart] + } } + + # 2 разных знач в одной группе и партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + before { values.times { counter.add(:param0 => 1, :param1 => 3, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # дубль знач в другой партиции + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart3) } } + # дубль знач в другой группе + before { values.times { counter.add(:param0 => 1, :param1 => 2, :group => :group1, :subgroup => :subgroup3, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подпартиции + before { values.times { counter.add(:param0 => 3, :param1 => 4, :group => :group1, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart2) } } + # новое значение в новой партиции + before { values.times { counter.add(:param0 => 4, :param1 => 5, :group => :group1, :subgroup => :subgroup1, :part => :part2, :subpart => :subpart1) } } + # новое значение в новой группе + before { values.times { counter.add(:param0 => 5, :param1 => 6, :group => :group2, :subgroup => :subgroup1, :part => :part1, :subpart => :subpart1) } } + # новое значение в новой подгруппе + before { values.times { counter.add(:param0 => 6, :param1 => 7, :group => :group1, :subgroup => :subgroup2, :part => :part1, :subpart => :subpart1) } } + + context 'when no group given' do + it { expect { counter.delete_all! }.to raise_error ArgumentError } + end + + context 'when no leaf group given' do + it { expect { counter.delete_all!(:group => :group1) }.to raise_error KeyError } + end + + context 'when unknown group given' do + before { counter.delete_all!({:group => :unknown_group, :subgroup => :subgroup}) } + + it { expect(counter.partitions({:group => :unknown_group, :subgroup => :subgroup})).to have(0).partitions } + end + + context 'when no partition given' do + before { counter.delete_all!(group1_subgroup1) } + + it { expect(counter.data(group1_subgroup1)).to have(0).rows } + it { expect(counter.data(group2_subgroup1)).to have(1).rows } + it { expect(counter.data(group1_subgroup2)).to have(1).rows } + it { expect(counter.data(group2_subgroup1)).to include ({'param0' => '5', 'param1' => '6'}) } + it { expect(counter.data(group1_subgroup2)).to include ({'param0' => '6', 'param1' => '7'}) } + + it { expect(counter.partitions(group1_subgroup1)).to have(0).partitions } + it { expect(counter.partitions(group2_subgroup1)).to have(1).partitions } + it { expect(counter.partitions(group2_subgroup1)).to have(1).partitions } + end end end end \ No newline at end of file