Skip to content

Commit

Permalink
feature(unique_values_list): introduced a list of unique values to ex…
Browse files Browse the repository at this point in the history
…piry of the members
  • Loading branch information
Napolskih committed Oct 22, 2014
1 parent e1206b7 commit 48deb40
Show file tree
Hide file tree
Showing 16 changed files with 517 additions and 83 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ test/version_tmp
tmp
.idea/
.rbx/
gemfiles/
gemfiles/
/coverage
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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}
61 changes: 57 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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]
})

Expand Down Expand Up @@ -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

Сборная конструкция на основе предыдущих.
Expand Down Expand Up @@ -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']
```
```
2 changes: 2 additions & 0 deletions lib/redis_counters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions lib/redis_counters/clusterize_and_partitionize.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 - хеш параметров, определяющий кластер и партицию.
Expand Down
9 changes: 5 additions & 4 deletions lib/redis_counters/unique_values_lists/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 17 additions & 11 deletions lib/redis_counters/unique_values_lists/blocking.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: Нетранзакционно удаляет данные конкретной конечной партиции.
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -141,7 +148,6 @@ def new_partition?
# Если партиция не указана, возвращает все партиции кластера (все партиции, если нет кластеризации).
#
# params - Hash - хеш параметров, определяющий кластер и партицию.
# parts - Array of Hash - список партиций.
#
# Returns Array of Hash.
#
Expand Down
Loading

0 comments on commit 48deb40

Please sign in to comment.