Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Napolskih committed Sep 11, 2013
1 parent 21ed53b commit 897418d
Show file tree
Hide file tree
Showing 24 changed files with 1,183 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .gitignore
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/
Empty file added CHANGELOG
Empty file.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'
source 'http://apress:[email protected]'

gemspec
180 changes: 180 additions & 0 deletions README.md
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']
```
14 changes: 14 additions & 0 deletions Rakefile
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)
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.0.0beta1
17 changes: 17 additions & 0 deletions lib/redis_counters.rb
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
44 changes: 44 additions & 0 deletions lib/redis_counters/base_counter.rb
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
50 changes: 50 additions & 0 deletions lib/redis_counters/hash_counter.rb
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
35 changes: 35 additions & 0 deletions lib/redis_counters/unique_hash_counter.rb
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
Loading

0 comments on commit 897418d

Please sign in to comment.