lib/redis_counters/unique_values_lists/expirable.rb
# 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