-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
1,183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
*.gem | ||
*.rbc | ||
.bundle | ||
.config | ||
.yardoc | ||
Gemfile.lock | ||
InstalledFiles | ||
_yardoc | ||
coverage | ||
doc/ | ||
lib/bundler/man | ||
pkg | ||
rdoc | ||
spec/reports | ||
test/tmp | ||
test/version_tmp | ||
tmp | ||
.idea/ | ||
.rbx/ | ||
gemfiles/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
source 'https://rubygems.org' | ||
source 'http://apress:[email protected]' | ||
|
||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
# 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/class_logger.png?circle-token=e4d0ed5c60a5ff795bf971229addb871552c2750)](https://circleci.com/gh/abak-press/redis_counters) | ||
|
||
Набор структур данных на базе Redis. | ||
|
||
## RedisCounters::HashCounter | ||
|
||
Счетчик на основе Hash, с ~~преферансом и тайками-близняшками~~ партиционированием и группировкой значений. | ||
|
||
Обязательные параметры: counter_name, field_name или group_keys. | ||
|
||
### Сложность | ||
+ инкремент - O(1). | ||
|
||
### Примеры использования | ||
|
||
Простой счетчик значений. | ||
```ruby | ||
counter = RedisCounters::HashCounter.new(redis, { | ||
:counter_name => :pages_by_day, | ||
:field_name => :pages | ||
}) | ||
|
||
5.times { counter.increment } | ||
|
||
redis: | ||
pages_by_day = { | ||
pages => 5 | ||
} | ||
``` | ||
|
||
Счетчик посещенных страниц компании с партиционированием по дате. | ||
```ruby | ||
counter = RedisCounters::HashCounter.new(redis, { | ||
:counter_name => :pages_by_day, | ||
:group_keys => [:company_id], | ||
:partition_keys => [:date] | ||
}) | ||
|
||
2.times { counter.increment(:company_id = 1, :date => '2013-08-01') } | ||
3.times { counter.increment(:company_id = 2, :date => '2013-08-01') } | ||
1.times { counter.increment(:company_id = 3, :date => '2013-08-02') } | ||
|
||
redis: | ||
pages_by_day:2013-08-01 = { | ||
1 => 2 | ||
2 => 3 | ||
} | ||
pages_by_day:2013-08-02 = { | ||
3 => 1 | ||
} | ||
``` | ||
|
||
Тоже самое, но партиция задается с помощью proc. | ||
```ruby | ||
counter = RedisCounters::HashCounter.new(redis, { | ||
:counter_name => :pages_by_day, | ||
:group_keys => [:company_id], | ||
:partition_keys => proc { |params| params.fetch(:date) } | ||
}) | ||
``` | ||
|
||
Счетчик посещенных страниц компании с группировкой по городу посетителя и партиционированием по дате. | ||
```ruby | ||
counter = RedisCounters::HashCounter.new(redis, { | ||
:counter_name => :pages_by_day, | ||
:group_keys => [:company_id, city_id], | ||
:partition_keys => [:date] | ||
}) | ||
|
||
2.times { counter.increment(:company_id = 1, :city_id => 11, :date => '2013-08-01') } | ||
1.times { counter.increment(:company_id = 1, :city_id => 12, :date => '2013-08-01') } | ||
3.times { counter.increment(:company_id = 2, :city_id => 11, :date => '2013-08-01') } | ||
|
||
redis: | ||
pages_by_day:2013-08-01 = { | ||
1:11 => 2, | ||
1:12 => 1, | ||
2_11 => 3 | ||
} | ||
``` | ||
|
||
## RedisCounters::UniqueValuesList | ||
|
||
Список уникальных значений, с возможностью группировки и партиционирования значений. | ||
Помимо списка значений, ведет так же, список партиций, для каждой группы. | ||
|
||
Обязательные параметры: counter_name и value_keys. | ||
|
||
### Сложность | ||
+ добавление элемента - от O(1), при отсутствии партиционирования, до O(N), где N - кол-во партиций. | ||
|
||
### Примеры использования | ||
|
||
Простой список уникальных пользователей. | ||
```ruby | ||
counter = RedisCounters::UniqueValuesList.new(redis, { | ||
:counter_name => :users, | ||
:value_keys => [:user_id] | ||
}) | ||
|
||
counter.increment(:user_id => 1) | ||
counter.increment(:user_id => 2) | ||
counter.increment(:user_id => 1) | ||
|
||
redis: | ||
users = ['1', '2'] | ||
``` | ||
|
||
Список уникальных пользователей, посетивших компаниию, за месяц, сгруппированный по суткам. | ||
```ruby | ||
counter = RedisCounters::UniqueValuesList.new(redis, { | ||
:counter_name => :company_users_by_month, | ||
:value_keys => [:company_id, :user_id], | ||
:group_keys => [:start_month_date], | ||
:partition_keys => [:date] | ||
}) | ||
|
||
2.times { counter.add(:company_id = 1, :user_id => 11, :date => '2013-08-10', :start_month_date => '2013-08-01') } | ||
3.times { counter.add(:company_id = 1, :user_id => 22, :date => '2013-08-10', :start_month_date => '2013-08-01') } | ||
3.times { counter.add(:company_id = 1, :user_id => 22, :date => '2013-09-05', :start_month_date => '2013-09-01') } | ||
3.times { counter.add(:company_id = 2, :user_id => 11, :date => '2013-08-10', :start_month_date => '2013-08-01') } | ||
1.times { counter.add(:company_id = 2, :user_id => 22, :date => '2013-08-11', :start_month_date => '2013-08-01') } | ||
|
||
redis: | ||
company_users_by_month:2013-08-01:partitions = ['2013-08-10', '2013-08-11'] | ||
company_users_by_month:2013-08-01:2013-08-10 = ['1:11', '1:22', '2:11'] | ||
company_users_by_month:2013-08-01:2013-08-11 = ['2:22'] | ||
|
||
company_users_by_month:2013-09-01:partitions = ['2013-09-05'] | ||
company_users_by_month:2013-09-01:2013-09-05 = ['1:22'] | ||
``` | ||
|
||
## RedisCounters::UniqueHashCounter | ||
|
||
Структура на основе двух предыдущих. | ||
HashCounter, с возможностью подсчета только у уникальных событий. | ||
|
||
### Сложность | ||
аналогично UniqueValuesList. | ||
|
||
### Примеры использования | ||
|
||
Счетчик уникальных пользователей, посетивших компаниию, за месяц, сгруппированный по суткам. | ||
```ruby | ||
counter = RedisCounters::UniqueHashCounter.new(redis, { | ||
:counter_name => :company_users_by_month, | ||
:group_keys => [:company_id], | ||
:partition_keys => [:date], | ||
:unique_list => { | ||
:value_keys => [:company_id, :user_id], | ||
:group_keys => [:start_month_date], | ||
:partition_keys => [:date] | ||
} | ||
}) | ||
|
||
2.times { counter.increment(:company_id = 1, :user_id => 11, :date => '2013-08-10', :start_month_date => '2013-08-01') } | ||
3.times { counter.increment(:company_id = 1, :user_id => 22, :date => '2013-08-10', :start_month_date => '2013-08-01') } | ||
3.times { counter.increment(:company_id = 1, :user_id => 22, :date => '2013-09-05', :start_month_date => '2013-09-01') } | ||
3.times { counter.increment(:company_id = 2, :user_id => 11, :date => '2013-08-10', :start_month_date => '2013-08-01') } | ||
1.times { counter.increment(:company_id = 2, :user_id => 22, :date => '2013-08-11', :start_month_date => '2013-08-01') } | ||
|
||
redis: | ||
company_users_by_month:2013-08-10 = { | ||
1 = 2, | ||
2 = 1 | ||
} | ||
company_users_by_month:2013-08-11 = { | ||
2 = 1 | ||
} | ||
company_users_by_month:2013-09-05 = { | ||
1 = 1 | ||
} | ||
|
||
company_users_by_month_uq:2013-08-01:partitions = ['2013-08-10', '2013-08-11'] | ||
company_users_by_month_uq:2013-08-01:2013-08-10 = ['1:11', '1:22', '2:11'] | ||
company_users_by_month_uq:2013-08-01:2013-08-11 = ['2:22'] | ||
|
||
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'] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# coding: utf-8 | ||
# load everything from tasks/ directory | ||
Dir[File.join(File.dirname(__FILE__), 'tasks', '*.{rb,rake}')].each { |f| load(f) } | ||
|
||
task :build => [:check] | ||
task :tag => :build | ||
|
||
desc 'Check if all projects are ready for build process' | ||
task :check => [:audit, :quality, :coverage] | ||
|
||
require 'rspec/core/rake_task' | ||
|
||
# setup `spec` task | ||
RSpec::Core::RakeTask.new(:spec) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
1.0.0beta1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
# encoding: utf-8 | ||
require 'redis_counters/version' | ||
require 'redis_counters/base_counter' | ||
require 'redis_counters/hash_counter' | ||
require 'redis_counters/unique_hash_counter' | ||
require 'redis_counters/unique_values_list' | ||
|
||
require 'active_support/core_ext' | ||
|
||
module RedisCounters | ||
|
||
def create_counter(redis, opts) | ||
BaseCounter.create(redis, opts) | ||
end | ||
|
||
module_function :create_counter | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# coding: utf-8 | ||
require 'forwardable' | ||
|
||
module RedisCounters | ||
|
||
class BaseCounter | ||
extend Forwardable | ||
|
||
KEY_DELIMITER = ':'.freeze | ||
|
||
attr_reader :redis | ||
attr_reader :options | ||
attr_reader :params | ||
|
||
def self.create(redis, opts) | ||
counter_class = opts.fetch(:counter_class).to_s.constantize | ||
counter_class.new(redis, opts) | ||
end | ||
|
||
def initialize(redis, opts) | ||
@redis = redis | ||
@options = opts | ||
init | ||
end | ||
|
||
def process(params = {}, &block) | ||
@params = params | ||
process_value(&block) | ||
end | ||
|
||
protected | ||
|
||
def init | ||
counter_name.present? | ||
end | ||
|
||
def counter_name | ||
@counter_name ||= options.fetch(:counter_name) | ||
end | ||
|
||
def_delegator :redis, :multi, :transaction | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# coding: utf-8 | ||
require 'redis_counters/base_counter' | ||
|
||
module RedisCounters | ||
|
||
class HashCounter < BaseCounter | ||
alias_method :increment, :process | ||
|
||
def init | ||
super | ||
return if field_name.present? || group_keys.present? | ||
raise ArgumentError, 'field_name or group_keys required!' | ||
end | ||
|
||
protected | ||
|
||
def process_value | ||
redis.hincrby(key, field, 1) | ||
end | ||
|
||
def key | ||
[counter_name, partition].flatten.join(KEY_DELIMITER) | ||
end | ||
|
||
def partition | ||
partition_keys.map do |key| | ||
key.respond_to?(:call) ? key.call(params) : params.fetch(key) | ||
end | ||
end | ||
|
||
def field | ||
group_params = group_keys.map { |key| params.fetch(key) } | ||
group_params << field_name if field_name.present? | ||
group_params.join(KEY_DELIMITER) | ||
end | ||
|
||
def field_name | ||
@field_name ||= options[:field_name] | ||
end | ||
|
||
def group_keys | ||
@group_keys ||= Array.wrap(options.fetch(:group_keys, [])) | ||
end | ||
|
||
def partition_keys | ||
@partition_keys ||= Array.wrap(options.fetch(:partition_keys, [])) | ||
end | ||
end | ||
|
||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# coding: utf-8 | ||
require 'redis_counters/hash_counter' | ||
require 'redis_counters/unique_values_list' | ||
|
||
module RedisCounters | ||
|
||
class UniqueHashCounter < HashCounter | ||
UNIQUE_LIST_POSTFIX = 'uq'.freeze | ||
|
||
protected | ||
|
||
def process_value | ||
unique_values_list.add(params) { super } | ||
end | ||
|
||
attr_reader :unique_values_list | ||
|
||
def init | ||
super | ||
@unique_values_list = UniqueValuesList.new( | ||
redis, | ||
unique_values_list_options | ||
) | ||
end | ||
|
||
def unique_values_list_options | ||
options.fetch(:unique_list).merge!(:counter_name => unique_values_list_name) | ||
end | ||
|
||
def unique_values_list_name | ||
[counter_name, UNIQUE_LIST_POSTFIX].join(KEY_DELIMITER) | ||
end | ||
end | ||
|
||
end |
Oops, something went wrong.