From 48deb4047990128fe2fe03b5d55d11239514d38e Mon Sep 17 00:00:00 2001 From: Artem Napolskih Date: Fri, 17 Oct 2014 17:26:40 +0600 Subject: [PATCH] feature(unique_values_list): introduced a list of unique values to expiry of the members --- .gitignore | 3 +- Makefile | 14 ++ README.md | 61 ++++++- lib/redis_counters.rb | 2 + .../clusterize_and_partitionize.rb | 14 ++ .../unique_values_lists/base.rb | 9 +- .../unique_values_lists/blocking.rb | 28 +-- .../unique_values_lists/expirable.rb | 155 +++++++++++++++++ .../unique_values_lists/non_blocking.rb | 15 +- redis_counters.gemspec | 3 +- .../unique_values_lists/blocking_spec.rb | 14 +- .../unique_values_lists/expirable_spec.rb | 6 + spec/spec_helper.rb | 6 +- spec/support/unique_values_lists/common.rb | 51 +++--- spec/support/unique_values_lists/expirable.rb | 162 ++++++++++++++++++ spec/support/unique_values_lists/set.rb | 57 +++--- 16 files changed, 517 insertions(+), 83 deletions(-) create mode 100644 Makefile create mode 100644 lib/redis_counters/unique_values_lists/expirable.rb create mode 100644 spec/redis_counters/unique_values_lists/expirable_spec.rb create mode 100644 spec/support/unique_values_lists/expirable.rb diff --git a/.gitignore b/.gitignore index 949fa6d..2e57dc7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ test/version_tmp tmp .idea/ .rbx/ -gemfiles/ \ No newline at end of file +gemfiles/ +/coverage \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e859e6b --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +BUNDLE = bundle +BUNDLE_OPTIONS = -j 4 +RSPEC = ${BUNDLE} exec rspec + +all: test + +test: bundler/install + ${RSPEC} 2>&1 + +bundler/install: + if ! gem list bundler -i > /dev/null; then \ + gem install bundler; \ + fi + ${BUNDLE} install ${BUNDLE_OPTIONS} \ No newline at end of file diff --git a/README.md b/README.md index 0431072..85e7b01 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# RedisCounters [![Code Climate](https://codeclimate.com/repos/522e9b497e00a46a0d01227c/badges/ae868ca76e52852ebc5a/gpa.png)](https://codeclimate.com/repos/522e9b497e00a46a0d01227c/feed) [![CircleCI](https://circleci.com/gh/abak-press/redis_counters.png?circle-token=546614f052a33b41e85b547c40ff74a15fcaf010)](https://circleci.com/gh/abak-press/redis_counters) +# RedisCounters + +[![Dolly](http://dolly.railsc.ru/badges/abak-press/redis_counters/master)](http://dolly.railsc.ru/projects/36/builds/latest/?ref=master) +[![Code Climate](https://codeclimate.com/repos/522e9b497e00a46a0d01227c/badges/ae868ca76e52852ebc5a/gpa.png)](https://codeclimate.com/repos/522e9b497e00a46a0d01227c/feed) +[![Test Coverage](https://codeclimate.com/repos/522e9b497e00a46a0d01227c/badges/ae868ca76e52852ebc5a/coverage.svg)](https://codeclimate.com/repos/522e9b497e00a46a0d01227c/feed) Набор структур данных на базе Redis. @@ -175,12 +179,12 @@ redis: users = ['1', '2'] ``` -Список уникальных пользователей, посетивших компаниию, за месяц, кластеризованный по суткам. +Список уникальных пользователей, посетивших компаниию, за месяц, партиционированный по суткам. ```ruby counter = RedisCounters::UniqueValuesLists::Blocking.new(redis, { :counter_name => :company_users_by_month, :value_keys => [:company_id, :user_id], - :cluster_keys => [:start_month_date], + :cluster_keys => [:start_month_date], :partition_keys => [:date] }) @@ -216,6 +220,55 @@ Eсли партиционирование не используется, то ### Сложность + добавление элемента - O(1) + +## RedisCounters::UniqueValuesLists::Expirable + +Список уникальных значений, с возможностью expire отдельных элементов. + +На основе сортированного множества. +http://redis4you.com/code.php?id=010 + +На основе механизма оптимистических блокировок. +смотри Optimistic locking using check-and-set: +http://redis.io/topics/transactions + +Особенности: +- Expire - таймаут, можно установить как на уровне счетчика, + так и на уровне отдельного занчения; +- Очистка возможна как в автоматическогом режиме так в и ручном; +- Значения сохраняет в партициях; +- Ведет список партиций; +- Полностью транзакционен. + +Обязательные параметры: counter_name и value_keys. +Таймаут задается параметром :expire. По умолчанию :never. +:clean_expired - режим автоочистки. По умолчанию true. + +### Примеры использования + +```ruby +counter = RedisCounters::UniqueValuesLists::Expirable.new(redis, + :counter_name => :sessions, + :value_keys => [:session_id], + :expire => 10.minutes +) + +counter << session_id: 1 +counter << session_id: 2 +counter << session_id: 3, expire: :never + +counter.data +> [{session_id: 1}, {session_id: 2}, {session_id: 3}] + +# after 10 minutes + +counter.data +> [{session_id: 3}] + +counter.has_value?(session_id: 1) +false +``` + ## RedisCounters::UniqueHashCounter Сборная конструкция на основе предыдущих. @@ -264,4 +317,4 @@ redis: company_users_by_month_uq:2013-09-01:partitions = ['2013-09-05'] company_users_by_month_uq:2013-09-01:2013-09-05 = ['1:22'] -``` +``` \ No newline at end of file diff --git a/lib/redis_counters.rb b/lib/redis_counters.rb index b3638aa..e2add2b 100644 --- a/lib/redis_counters.rb +++ b/lib/redis_counters.rb @@ -6,7 +6,9 @@ require 'redis_counters/unique_values_lists/base' require 'redis_counters/unique_values_lists/blocking' require 'redis_counters/unique_values_lists/non_blocking' +require 'redis_counters/unique_values_lists/expirable' +require 'active_support' require 'active_support/core_ext' module RedisCounters diff --git a/lib/redis_counters/clusterize_and_partitionize.rb b/lib/redis_counters/clusterize_and_partitionize.rb index a243b0d..118886f 100644 --- a/lib/redis_counters/clusterize_and_partitionize.rb +++ b/lib/redis_counters/clusterize_and_partitionize.rb @@ -129,6 +129,9 @@ def delete_all_direct!(cluster, write_session = redis, parts = partitions(cluste protected def key(partition = partition_params, cluster = cluster_params) + raise 'Array required' if partition && !partition.is_a?(Array) + raise 'Array required' if cluster && !cluster.is_a?(Array) + [counter_name, cluster, partition].flatten.join(key_delimiter) end @@ -154,6 +157,17 @@ def use_partitions? partition_keys.present? end + def set_params(params) + @params = params.with_indifferent_access + check_cluster_params + end + + def form_cluster_params(cluster_params = params) + RedisCounters::Cluster.new(self, cluster_params).params + end + + alias_method :check_cluster_params, :form_cluster_params + # Protected: Возвращает массив листовых партиций в виде ключей. # # params - Hash - хеш параметров, определяющий кластер и партицию. diff --git a/lib/redis_counters/unique_values_lists/base.rb b/lib/redis_counters/unique_values_lists/base.rb index 853b0d8..9a6048f 100644 --- a/lib/redis_counters/unique_values_lists/base.rb +++ b/lib/redis_counters/unique_values_lists/base.rb @@ -12,21 +12,22 @@ class Base < RedisCounters::BaseCounter include RedisCounters::ClusterizeAndPartitionize alias_method :add, :process + alias_method :<<, :process # Public: Проверяет существует ли заданное значение. # - # value_params - Hash - параметры значения. + # params - Hash - параметры кластера и значения. # # Returns Boolean. # - def has_value?(value_params) + def has_value?(params) raise NotImplementedError end protected - def value(value_params = params) - value_params = value_keys.map { |key| value_params.fetch(key) } + def value + value_params = value_keys.map { |key| params.fetch(key) } value_params.join(value_delimiter) end diff --git a/lib/redis_counters/unique_values_lists/blocking.rb b/lib/redis_counters/unique_values_lists/blocking.rb index 343e7c1..bae0076 100644 --- a/lib/redis_counters/unique_values_lists/blocking.rb +++ b/lib/redis_counters/unique_values_lists/blocking.rb @@ -19,14 +19,14 @@ class Blocking < Base # Public: Проверяет существует ли заданное значение. # - # value_params - Hash - параметры значения. + # params - Hash - параметры кластера и значения. # # Returns Boolean. # - def has_value?(value_params) - all_partitions.reverse.any? do |partition| - redis.sismember(key(partition), value(value_params)) - end + def has_value?(params) + set_params(params) + reset_partitions_cache + value_already_exists? end # Public: Нетранзакционно удаляет данные конкретной конечной партиции. @@ -57,6 +57,10 @@ def delete_partition_direct!(params = {}, write_session = redis) def key(partition = partition_params, cluster = cluster_params) return super if use_partitions? + + raise 'Array required' if partition && !partition.is_a?(Array) + raise 'Array required' if cluster && !cluster.is_a?(Array) + [counter_name, cluster, partition].flatten.compact.join(key_delimiter) end @@ -67,7 +71,7 @@ def process_value watch_partitions_list watch_all_partitions - if current_value_already_exists? + if value_already_exists? redis.unwatch return false end @@ -97,8 +101,10 @@ def watch_all_partitions end end - def current_value_already_exists? - has_value?(params) + def value_already_exists? + all_partitions.reverse.any? do |partition| + redis.sismember(key(partition), value) + end end def add_value @@ -112,8 +118,7 @@ def all_partitions(cluster = cluster_params) @partitions = redis.lrange(partitions_list_key(cluster), 0, -1) @partitions = @partitions.map do |partition| partition.split(key_delimiter, -1) - end - .delete_if(&:empty?) + end.delete_if(&:empty?) end def add_partition @@ -123,6 +128,8 @@ def add_partition end def partitions_list_key(cluster = cluster_params) + raise 'Array required' if cluster && !cluster.is_a?(Array) + [counter_name, cluster, PARTITIONS_LIST_POSTFIX].flatten.join(key_delimiter) end @@ -141,7 +148,6 @@ def new_partition? # Если партиция не указана, возвращает все партиции кластера (все партиции, если нет кластеризации). # # params - Hash - хеш параметров, определяющий кластер и партицию. - # parts - Array of Hash - список партиций. # # Returns Array of Hash. # diff --git a/lib/redis_counters/unique_values_lists/expirable.rb b/lib/redis_counters/unique_values_lists/expirable.rb new file mode 100644 index 0000000..23a592e --- /dev/null +++ b/lib/redis_counters/unique_values_lists/expirable.rb @@ -0,0 +1,155 @@ +# coding: utf-8 + +require 'redis_counters/unique_values_lists/blocking' +require 'active_support/core_ext/module/aliasing' + +module RedisCounters + module UniqueValuesLists + + # Список уникальных значений, с возможностью expire отдельных элементов. + # + # На основе сортированного множества. + # http://redis4you.com/code.php?id=010 + # + # На основе механизма оптимистических блокировок. + # смотри Optimistic locking using check-and-set: + # http://redis.io/topics/transactions + # + # Особенности: + # * Expire - таймаут, можно установить как на уровне счетчика, + # так и на уровне отдельного занчения; + # * Очистка возможна как в автоматическогом режиме так в и ручном; + # * Значения сохраняет в партициях; + # * Ведет список партиций; + # * Полностью транзакционен. + # + # Пример: + # + # counter = RedisCounters::UniqueValuesLists::Expirable.new(redis, + # :counter_name => :sessions, + # :value_keys => [:session_id], + # :expire => 10.minutes + # ) + # + # counter << session_id: 1 + # counter << session_id: 2 + # counter << session_id: 3, expire: :never + # + # counter.data + # > [{session_id: 1}, {session_id: 2}, {session_id: 3}] + # + # # after 10 minutes + # + # counter.data + # > [{session_id: 3}] + # + # counter.has_value?(session_id: 1) + # false + + class Expirable < Blocking + DEFAULT_AUTO_CLEAN_EXPIRED = true + DEFAULT_VALUE_TIMEOUT = :never + + NEVER_EXPIRE_TIMESTAMP = 0 + + # Public: Производит принудительную очистку expired - значений. + # + # cluster - Hash - параметры кластера, если используется кластеризация. + # + # Returns nothing. + # + def clean_expired(cluster = {}) + set_params(cluster) + internal_clean_expired + end + + protected + + def add_value + redis.zadd(key, value_expire_timestamp, value) + end + + def reset_partitions_cache + super + internal_clean_expired if auto_clean_expired? + end + + alias_method :clean, :reset_partitions_cache + + def current_timestamp + Time.now.to_i + end + + def value_already_exists? + all_partitions.reverse.any? do |partition| + redis.zrank(key(partition), value).present? + end + end + + def internal_clean_expired + all_partitions.each do |partition| + redis.zremrangebyscore(key(partition), "(#{NEVER_EXPIRE_TIMESTAMP}", current_timestamp) + end + end + + def value_expire_timestamp + timeout = params[:expire] || default_value_expire + + case timeout + when Symbol + NEVER_EXPIRE_TIMESTAMP + else + current_timestamp + timeout.to_i + end + end + + def default_value_expire + @default_value_expire ||= options[:expire].try(:seconds) || DEFAULT_VALUE_TIMEOUT + end + + def auto_clean_expired? + @auto_clean_expired ||= options.fetch(:clean_expired, DEFAULT_AUTO_CLEAN_EXPIRED) + end + + def partitions_with_clean(params = {}) + clean_empty_partitions(params) + partitions_without_clean(params) + end + + alias_method_chain :partitions, :clean + + # Protected: Производит очистку expired - значений и пустых партиций. + # + # params - Hash - параметры кластера, если используется кластеризация. + # + # Returns nothing. + # + def clean_empty_partitions(params) + set_params(params) + clean + + partitions_without_clean(params).each do |partition| + next if redis.zcard(key(partition.values)).nonzero? + delete_partition_direct!(params.merge(partition)) + end + end + + # Protected: Возвращает данные партиции в виде массива хешей. + # + # Каждый элемент массива, представлен в виде хеша, содержащего все параметры уникального значения. + # + # cluster - Array - листовой кластер - массив параметров однозначно идентифицирующий кластер. + # partition - Array - листовая партиция - массив параметров однозначно идентифицирующий партицию. + # + # Returns Array of WithIndifferentAccess. + # + def partition_data(cluster, partition) + keys = value_keys + redis.zrangebyscore(key(partition, cluster), '-inf', '+inf').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 +end \ No newline at end of file diff --git a/lib/redis_counters/unique_values_lists/non_blocking.rb b/lib/redis_counters/unique_values_lists/non_blocking.rb index 62af157..a0aa718 100644 --- a/lib/redis_counters/unique_values_lists/non_blocking.rb +++ b/lib/redis_counters/unique_values_lists/non_blocking.rb @@ -20,12 +20,13 @@ class NonBlocking < Base # Public: Проверяет существует ли заданное значение. # - # value_params - Hash - параметры значения. + # params - Hash - параметры кластера и значения. # # Returns Boolean. # - def has_value?(value_params) - redis.sismember(main_partition_key, value(value_params)) + def has_value?(params) + set_params(params) + redis.sismember(main_partition_key, value) end # Public: Нетранзакционно удаляет все данные счетчика в кластере, включая основную партицию. @@ -55,7 +56,7 @@ def delete_all_direct!(cluster, write_session = redis, parts = partitions(cluste # def delete_main_partition!(cluster = {}, write_session = redis) cluster = ::RedisCounters::Cluster.new(self, cluster).params - key = key([], cluster) + key = key(main_partition, cluster) write_session.del(key) end @@ -74,12 +75,16 @@ def add_value end def main_partition_key - key([]) + key(main_partition) end def current_partition_key key end + + def main_partition + [] + end end end diff --git a/redis_counters.gemspec b/redis_counters.gemspec index 233ec8f..79597a2 100644 --- a/redis_counters.gemspec +++ b/redis_counters.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |gem| gem.add_development_dependency 'rspec', '~> 2.14.0' gem.add_development_dependency 'mock_redis' gem.add_development_dependency 'timecop' + gem.add_development_dependency 'codeclimate-test-reporter', '~> 0.4.1' # test coverage tool gem.add_development_dependency 'simplecov' @@ -38,4 +39,4 @@ Gem::Specification.new do |gem| # a tool for uploading files to private gem repo gem.add_development_dependency 'multipart-post' -end \ No newline at end of file +end diff --git a/spec/redis_counters/unique_values_lists/blocking_spec.rb b/spec/redis_counters/unique_values_lists/blocking_spec.rb index 2881036..77edff9 100644 --- a/spec/redis_counters/unique_values_lists/blocking_spec.rb +++ b/spec/redis_counters/unique_values_lists/blocking_spec.rb @@ -13,10 +13,10 @@ context 'when group and partition keys given' do let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1], - :cluster_keys => [:param2], - :partition_keys => [:param3, :param4] + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :cluster_keys => [:param2], + :partition_keys => [:param3, :param4] } } before { values.times { counter.process(:param0 => 1, :param1 => 2, :param2 => :cluster1, :param3 => :part1, :param4 => :part2) } } @@ -56,9 +56,9 @@ context 'when no cluster keys given, but partition keys given' do let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1], - :partition_keys => [:param3, :param4] + :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) } } diff --git a/spec/redis_counters/unique_values_lists/expirable_spec.rb b/spec/redis_counters/unique_values_lists/expirable_spec.rb new file mode 100644 index 0000000..080c4eb --- /dev/null +++ b/spec/redis_counters/unique_values_lists/expirable_spec.rb @@ -0,0 +1,6 @@ +require 'spec_helper' + +describe RedisCounters::UniqueValuesLists::Expirable do + it_behaves_like 'unique_values_lists/common' + it_behaves_like 'unique_values_lists/expirable' +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8c5984e..c2cdec2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,9 +1,13 @@ require 'rubygems' require 'bundler/setup' require 'rspec' -require 'simplecov' require 'mock_redis' +require 'timecop' + +require 'codeclimate-test-reporter' +CodeClimate::TestReporter.start +require 'simplecov' SimpleCov.start('test_frameworks') require 'redis_counters' diff --git a/spec/support/unique_values_lists/common.rb b/spec/support/unique_values_lists/common.rb index f2fff0a..c870ca7 100644 --- a/spec/support/unique_values_lists/common.rb +++ b/spec/support/unique_values_lists/common.rb @@ -67,21 +67,28 @@ end context '#has_value?' do - let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0] - } } + context 'when cluster keys given and partition keys given' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:param0], + :cluster_keys => [:cluster], + :partition_keys => [:part] + } } - context 'when item not exists' do - it { expect(counter.has_value?(:param0 => 1)).to be_false } - end + let(:cluster1) { {:cluster => :cluster1} } + let(:cluster2) { {:cluster => :cluster2} } - context 'when item exists' do - before { counter.add(:param0 => 1) } + before { values.times { counter.add(:param0 => 1, :cluster => :cluster1, :part => :part1) } } + before { values.times { counter.add(:param0 => 2, :cluster => :cluster2, :part => :part2) } } - it { expect(counter.has_value?(:param0 => 1)).to be_true } - it { expect(counter.has_value?(:param0 => 2)).to be_false } + it { expect(counter.has_value?(cluster1.merge(:param0 => 1))).to be_true } + it { expect(counter.has_value?(cluster1.merge(:param0 => 2))).to be_false } + it { expect(counter.has_value?(cluster2.merge(:param0 => 1))).to be_false } + it { expect(counter.has_value?(cluster2.merge(:param0 => 2))).to be_true } end + + # check no cluster raise + # check unknown cluster raise end context '#partitions' do @@ -254,17 +261,17 @@ # новое значение в новом подкластере before { values.times { counter.add(:param0 => 6, :param1 => 7, :cluster => :cluster1, :subcluster => :subcluster2, :part => :part1, :subpart => :subpart1) } } - # context 'when no cluster given' do - # it { expect { counter.data }.to raise_error ArgumentError } - # end - # - # context 'when no leaf cluster given' do - # it { expect { counter.data(:cluster => :cluster1) }.to raise_error KeyError } - # end - # - # context 'when unknown cluster given' do - # it { expect(counter.data(:cluster => :unknown_cluster, :subcluster => :subcluster)).to have(0).partitions } - # end + context 'when no cluster given' do + it { expect { counter.data }.to raise_error ArgumentError } + end + + context 'when no leaf cluster given' do + it { expect { counter.data(:cluster => :cluster1) }.to raise_error KeyError } + end + + context 'when unknown cluster given' do + it { expect(counter.data(:cluster => :unknown_cluster, :subcluster => :subcluster)).to have(0).partitions } + end context 'when no partition given' do it { expect(counter.data(cluster1_subcluster1)).to have(4).rows } diff --git a/spec/support/unique_values_lists/expirable.rb b/spec/support/unique_values_lists/expirable.rb new file mode 100644 index 0000000..388c5d9 --- /dev/null +++ b/spec/support/unique_values_lists/expirable.rb @@ -0,0 +1,162 @@ +# coding: utf-8 +shared_examples_for 'unique_values_lists/expirable' do + let(:redis) { MockRedis.new } + let(:counter) { described_class.new(redis, options) } + + after do + Timecop.return + end + + context 'when auto expire enabled' do + context 'when expire given in counter options' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:value], + :partition_keys => [:part], + :expire => 10.seconds + } } + + before do + counter.add(:value => 1, :part => :part1) + counter.add(:value => 2, :part => :part2) + counter.add(:value => 3, :part => :part1, :expire => :never) + counter.add(:value => 4, :part => :part1, :expire => 20.seconds) + end + + context 'before time has expired' do + before { Timecop.freeze(5.seconds.since) } + + it { expect(counter.has_value?(:value => 1)).to be_true } + it { expect(counter.has_value?(:value => 2)).to be_true } + it { expect(counter.has_value?(:value => 3)).to be_true } + it { expect(counter.has_value?(:value => 4)).to be_true } + + it { expect(counter.partitions).to include('part' => 'part1') } + it { expect(counter.partitions).to include('part' => 'part2') } + + it { expect(counter.data).to include('value' => '1') } + it { expect(counter.data).to include('value' => '2') } + it { expect(counter.data).to include('value' => '3') } + it { expect(counter.data).to include('value' => '4') } + end + + context 'after time has expired' do + before { Timecop.freeze(10.seconds.since) } + + it { expect(counter.has_value?(:value => 1)).to be_false } + it { expect(counter.has_value?(:value => 2)).to be_false } + it { expect(counter.has_value?(:value => 3)).to be_true } + + it { expect(counter.partitions).to include('part' => 'part1') } + it { expect(counter.partitions).to_not include('part' => 'part2') } + + it { expect(counter.data).to_not include('value' => '1') } + it { expect(counter.data).to_not include('value' => '2') } + it { expect(counter.data).to include('value' => '3') } + it { expect(counter.data).to include('value' => '4') } + end + end + + context 'when expire not given in counter options' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:value], + :partition_keys => [:part] + } } + + before do + counter.add(:value => 1, :part => :part1, :expire => 10.seconds) + counter.add(:value => 2, :part => :part2, :expire => 10.seconds) + counter.add(:value => 3, :part => :part1) + counter.add(:value => 4, :part => :part1, :expire => 20.seconds) + end + + context 'before time has expired' do + before { Timecop.freeze(5.seconds.since) } + + it { expect(counter.has_value?(:value => 1)).to be_true } + it { expect(counter.has_value?(:value => 2)).to be_true } + it { expect(counter.has_value?(:value => 3)).to be_true } + it { expect(counter.has_value?(:value => 4)).to be_true } + + it { expect(counter.partitions).to include('part' => 'part1') } + it { expect(counter.partitions).to include('part' => 'part2') } + + it { expect(counter.data).to include('value' => '1') } + it { expect(counter.data).to include('value' => '2') } + it { expect(counter.data).to include('value' => '3') } + it { expect(counter.data).to include('value' => '4') } + end + + context 'after time has expired' do + before { Timecop.freeze(10.seconds.since) } + + it { expect(counter.has_value?(:value => 1)).to be_false } + it { expect(counter.has_value?(:value => 2)).to be_false } + it { expect(counter.has_value?(:value => 3)).to be_true } + it { expect(counter.has_value?(:value => 4)).to be_true } + + it { expect(counter.partitions).to include('part' => 'part1') } + it { expect(counter.partitions).to_not include('part' => 'part2') } + + it { expect(counter.data).to_not include('value' => '1') } + it { expect(counter.data).to_not include('value' => '2') } + it { expect(counter.data).to include('value' => '3') } + it { expect(counter.data).to include('value' => '4') } + end + end + end + + context 'when auto expire disabled' do + context 'when expire given in counter options' do + let(:options) { { + :counter_name => :test_counter, + :value_keys => [:value], + :partition_keys => [:part], + :expire => 10.seconds, + :clean_expired => false + } } + + before do + counter.add(:value => 1, :part => :part1) + counter.add(:value => 2, :part => :part2) + counter.add(:value => 3, :part => :part1, :expire => :never) + counter.add(:value => 4, :part => :part1, :expire => 20.seconds) + + Timecop.freeze(10.seconds.since) + end + + context 'after time has expired' do + it { expect(counter.has_value?(:value => 1)).to be_true } + it { expect(counter.has_value?(:value => 2)).to be_true } + it { expect(counter.has_value?(:value => 3)).to be_true } + it { expect(counter.has_value?(:value => 4)).to be_true } + + it { expect(counter.partitions).to include('part' => 'part1') } + it { expect(counter.partitions).to include('part' => 'part2') } + + it { expect(counter.data).to include('value' => '1') } + it { expect(counter.data).to include('value' => '2') } + it { expect(counter.data).to include('value' => '3') } + it { expect(counter.data).to include('value' => '4') } + end + + context 'when clean_expired call' do + before { counter.clean_expired } + + it { expect(counter.has_value?(:value => 1)).to be_false } + it { expect(counter.has_value?(:value => 2)).to be_false } + it { expect(counter.has_value?(:value => 3)).to be_true } + it { expect(counter.has_value?(:value => 4)).to be_true } + + it { expect(counter.partitions).to include('part' => 'part1') } + it { expect(counter.partitions).to_not include('part' => 'part2') } + + it { expect(counter.data).to_not include('value' => '1') } + it { expect(counter.data).to_not include('value' => '2') } + it { expect(counter.data).to include('value' => '3') } + it { expect(counter.data).to include('value' => '4') } + end + end + end +end \ No newline at end of file diff --git a/spec/support/unique_values_lists/set.rb b/spec/support/unique_values_lists/set.rb index f6a9b51..f5c7331 100644 --- a/spec/support/unique_values_lists/set.rb +++ b/spec/support/unique_values_lists/set.rb @@ -3,11 +3,6 @@ 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 '#add' do @@ -15,14 +10,16 @@ let(:options) { { :counter_name => :test_counter, :value_keys => [:param0, :param1], - :cluster_keys => [:param2], + :cluster_keys => [:param2], :partition_keys => [:param3, :param4] } } - before { values.times { counter.add(:param0 => 1, :param1 => 2, :param2 => :cluster1, :param3 => :part1, :param4 => :part2) } } - before { values.times { counter.add(:param0 => 2, :param1 => 1, :param2 => :cluster1, :param3 => :part1, :param4 => :part2) } } - before { values.times { counter.add(:param0 => 3, :param1 => 2, :param2 => :cluster1, :param3 => :part2, :param4 => :part2) } } - before { values.times { counter.add(:param0 => 4, :param1 => 5, :param2 => :cluster2, :param3 => :part1, :param4 => :part2) } } + before do + values.times { counter.add(:param0 => 1, :param1 => 2, :param2 => :cluster1, :param3 => :part1, :param4 => :part2) } + values.times { counter.add(:param0 => 2, :param1 => 1, :param2 => :cluster1, :param3 => :part1, :param4 => :part2) } + values.times { counter.add(:param0 => 3, :param1 => 2, :param2 => :cluster1, :param3 => :part2, :param4 => :part2) } + values.times { counter.add(:param0 => 4, :param1 => 5, :param2 => :cluster2, :param3 => :part1, :param4 => :part2) } + end it { expect(redis.keys('*')).to have(5).key } @@ -42,14 +39,16 @@ context 'when cluster and partition keys no given' do let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1] + :counter_name => :test_counter, + :value_keys => [:param0, :param1] } } - 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) } } + before do + values.times { counter.add(:param0 => 1, :param1 => 2) } + values.times { counter.add(:param0 => 1, :param1 => 2) } + values.times { counter.add(:param0 => 2, :param1 => 1) } + values.times { counter.add(:param0 => 3, :param1 => 2) } + end it { expect(redis.keys('*')).to have(1).key } @@ -68,10 +67,12 @@ :partition_keys => [:param3, :param4] } } - 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) } } + before do + values.times { counter.add(:param0 => 1, :param1 => 2, :param3 => :part1, :param4 => :part2) } + values.times { counter.add(:param0 => 2, :param1 => 1, :param3 => :part1, :param4 => :part2) } + values.times { counter.add(:param0 => 3, :param1 => 2, :param3 => :part2, :param4 => :part2) } + values.times { counter.add(:param0 => 4, :param1 => 5, :param3 => :part1, :param4 => :part2) } + end it { expect(redis.keys('*')).to have(3).key } @@ -89,15 +90,17 @@ context 'when cluster keys given, but partition keys not given' do let(:options) { { - :counter_name => :test_counter, - :value_keys => [:param0, :param1], - :cluster_keys => [:param2] + :counter_name => :test_counter, + :value_keys => [:param0, :param1], + :cluster_keys => [:param2] } } - before { values.times { counter.add(:param0 => 1, :param1 => 2, :param2 => :cluster1) } } - before { values.times { counter.add(:param0 => 2, :param1 => 1, :param2 => :cluster1) } } - before { values.times { counter.add(:param0 => 3, :param1 => 2, :param2 => :cluster1) } } - before { values.times { counter.add(:param0 => 4, :param1 => 5, :param2 => :cluster2) } } + before do + values.times { counter.add(:param0 => 1, :param1 => 2, :param2 => :cluster1) } + values.times { counter.add(:param0 => 2, :param1 => 1, :param2 => :cluster1) } + values.times { counter.add(:param0 => 3, :param1 => 2, :param2 => :cluster1) } + values.times { counter.add(:param0 => 4, :param1 => 5, :param2 => :cluster2) } + end it { expect(redis.keys('*')).to have(2).key }