abak-press/redis_counters

View on GitHub
lib/redis_counters/unique_values_lists/expirable.rb

Summary

Maintainability
A
0 mins
Test Coverage
# 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