redisrank 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.gitignore +27 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +297 -0
- data/Rakefile +69 -0
- data/lib/redisrank.rb +106 -0
- data/lib/redisrank/buffer.rb +110 -0
- data/lib/redisrank/collection.rb +20 -0
- data/lib/redisrank/connection.rb +89 -0
- data/lib/redisrank/core_ext.rb +5 -0
- data/lib/redisrank/core_ext/bignum.rb +8 -0
- data/lib/redisrank/core_ext/date.rb +8 -0
- data/lib/redisrank/core_ext/fixnum.rb +8 -0
- data/lib/redisrank/core_ext/hash.rb +20 -0
- data/lib/redisrank/core_ext/time.rb +3 -0
- data/lib/redisrank/date.rb +88 -0
- data/lib/redisrank/event.rb +98 -0
- data/lib/redisrank/finder.rb +245 -0
- data/lib/redisrank/finder/date_set.rb +99 -0
- data/lib/redisrank/key.rb +84 -0
- data/lib/redisrank/label.rb +69 -0
- data/lib/redisrank/mixins/database.rb +11 -0
- data/lib/redisrank/mixins/date_helper.rb +8 -0
- data/lib/redisrank/mixins/options.rb +41 -0
- data/lib/redisrank/mixins/synchronize.rb +52 -0
- data/lib/redisrank/model.rb +77 -0
- data/lib/redisrank/result.rb +18 -0
- data/lib/redisrank/scope.rb +18 -0
- data/lib/redisrank/summary.rb +90 -0
- data/lib/redisrank/version.rb +3 -0
- data/redisrank.gemspec +31 -0
- data/spec/Find Results +3349 -0
- data/spec/buffer_spec.rb +104 -0
- data/spec/collection_spec.rb +20 -0
- data/spec/connection_spec.rb +67 -0
- data/spec/core_ext/hash_spec.rb +26 -0
- data/spec/database_spec.rb +10 -0
- data/spec/date_spec.rb +95 -0
- data/spec/event_spec.rb +86 -0
- data/spec/finder/date_set_spec.rb +527 -0
- data/spec/finder_spec.rb +205 -0
- data/spec/key_spec.rb +129 -0
- data/spec/label_spec.rb +86 -0
- data/spec/model_helper.rb +31 -0
- data/spec/model_spec.rb +191 -0
- data/spec/options_spec.rb +36 -0
- data/spec/redis-test.conf +9 -0
- data/spec/result_spec.rb +23 -0
- data/spec/scope_spec.rb +27 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/summary_spec.rb +177 -0
- data/spec/synchronize_spec.rb +125 -0
- data/spec/thread_safety_spec.rb +39 -0
- metadata +235 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'redisrank/core_ext/hash'
|
2
|
+
|
3
|
+
module Redisrank
|
4
|
+
class Buffer
|
5
|
+
include Synchronize
|
6
|
+
|
7
|
+
def self.instance
|
8
|
+
@instance ||= self.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def size
|
12
|
+
synchronize do
|
13
|
+
@size ||= 0
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def size=(value)
|
18
|
+
synchronize do
|
19
|
+
@size = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def count
|
24
|
+
@count ||= 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def store(key, stats, depth_limit, opts)
|
28
|
+
return false unless should_buffer?
|
29
|
+
|
30
|
+
to_flush = {}
|
31
|
+
buffkey = buffer_key(key, opts)
|
32
|
+
|
33
|
+
synchronize do
|
34
|
+
if !queue.has_key?(buffkey)
|
35
|
+
queue[buffkey] = { :key => key,
|
36
|
+
:stats => {},
|
37
|
+
:depth_limit => depth_limit,
|
38
|
+
:opts => opts }
|
39
|
+
end
|
40
|
+
|
41
|
+
queue[buffkey][:stats].merge_to_max!(stats)
|
42
|
+
incr_count
|
43
|
+
|
44
|
+
# return items to be flushed if buffer size limit has been reached
|
45
|
+
to_flush = reset_queue
|
46
|
+
end
|
47
|
+
|
48
|
+
# flush any data that's been cleared from the queue
|
49
|
+
flush_data(to_flush)
|
50
|
+
true
|
51
|
+
end
|
52
|
+
|
53
|
+
def flush(force = false)
|
54
|
+
to_flush = {}
|
55
|
+
synchronize do
|
56
|
+
to_flush = reset_queue(force)
|
57
|
+
end
|
58
|
+
flush_data(to_flush)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# should always be called from within a synchronize block
|
64
|
+
def incr_count
|
65
|
+
@count ||= 0
|
66
|
+
@count += 1
|
67
|
+
end
|
68
|
+
|
69
|
+
def queue
|
70
|
+
@queue ||= {}
|
71
|
+
end
|
72
|
+
|
73
|
+
def should_buffer?
|
74
|
+
size > 1 # buffer size of 1 would be equal to not using buffer
|
75
|
+
end
|
76
|
+
|
77
|
+
# should always be called from within a synchronize block
|
78
|
+
def should_flush?
|
79
|
+
(!queue.blank? && count >= size)
|
80
|
+
end
|
81
|
+
|
82
|
+
# returns items to be flushed if buffer size limit has been reached
|
83
|
+
# should always be called from within a synchronize block
|
84
|
+
def reset_queue(force = false)
|
85
|
+
return {} if !force && !should_flush?
|
86
|
+
data = queue
|
87
|
+
@queue = {}
|
88
|
+
@count = 0
|
89
|
+
data
|
90
|
+
end
|
91
|
+
|
92
|
+
def flush_data(buffer_data)
|
93
|
+
buffer_data.each do |k, item|
|
94
|
+
Summary.update(item[:key], item[:stats], item[:depth_limit], item[:opts])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# depth_limit is not needed as it's evident in key.to_s
|
99
|
+
def buffer_key(key, opts)
|
100
|
+
# covert keys to strings, as sorting a Hash with Symbol keys fails on
|
101
|
+
# Ruby 1.8.x.
|
102
|
+
opts = opts.inject({}) do |result, (k, v)|
|
103
|
+
result[k.to_s] = v
|
104
|
+
result
|
105
|
+
end
|
106
|
+
"#{key.to_s}:#{opts.sort.flatten.join(':')}"
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Redisrank
|
2
|
+
class Collection < ::Array
|
3
|
+
|
4
|
+
attr_accessor :from
|
5
|
+
attr_accessor :till
|
6
|
+
attr_accessor :depth
|
7
|
+
attr_accessor :rank
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@from = options[:from] ||= nil
|
11
|
+
@till = options[:till] ||= nil
|
12
|
+
@depth = options[:depth] ||= nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def rank
|
16
|
+
@rank ||= {}
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module Redisrank
|
4
|
+
module Connection
|
5
|
+
|
6
|
+
REQUIRED_SERVER_VERSION = "1.3.10"
|
7
|
+
MIN_EXPIRE_SERVER_VERSION = "2.1.3"
|
8
|
+
|
9
|
+
# TODO: Create a ConnectionPool instance object using Sychronize mixin to replace Connection class
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# TODO: clean/remove all ref-less connections
|
14
|
+
|
15
|
+
def get(ref = nil)
|
16
|
+
ref ||= :default
|
17
|
+
synchronize do
|
18
|
+
connections[references[ref]] || create
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add(conn, ref = nil)
|
23
|
+
ref ||= :default
|
24
|
+
synchronize do
|
25
|
+
check_redis_version(conn)
|
26
|
+
references[ref] = conn.client.id
|
27
|
+
connections[conn.client.id] = conn
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def create(options = {})
|
32
|
+
synchronize do
|
33
|
+
options = options.clone
|
34
|
+
ref = options.delete(:ref) || :default
|
35
|
+
options.reverse_merge!(default_options)
|
36
|
+
conn = (connections[connection_id(options)] ||= connection(options))
|
37
|
+
references[ref] = conn.client.id
|
38
|
+
conn
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def connections
|
43
|
+
@connections ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
def references
|
47
|
+
@references ||= {}
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def monitor
|
53
|
+
@monitor ||= Monitor.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def synchronize(&block)
|
57
|
+
monitor.synchronize(&block)
|
58
|
+
end
|
59
|
+
|
60
|
+
def connection(options)
|
61
|
+
check_redis_version(Redis.new(options))
|
62
|
+
end
|
63
|
+
|
64
|
+
def connection_id(options = {})
|
65
|
+
options = options.reverse_merge(default_options)
|
66
|
+
"redis://#{options[:host]}:#{options[:port]}/#{options[:db]}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def check_redis_version(conn)
|
70
|
+
raise RedisServerIsTooOld if conn.info["redis_version"] < REQUIRED_SERVER_VERSION
|
71
|
+
if conn.info["redis_version"] < MIN_EXPIRE_SERVER_VERSION
|
72
|
+
STDOUT.puts "WARNING: You MUST upgrade Redis to v2.1.3 or later " +
|
73
|
+
"if you are using key expiry."
|
74
|
+
end
|
75
|
+
conn
|
76
|
+
end
|
77
|
+
|
78
|
+
def default_options
|
79
|
+
{
|
80
|
+
:host => '127.0.0.1',
|
81
|
+
:port => 6379,
|
82
|
+
:db => 0,
|
83
|
+
:timeout => 5
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
def merge_to_max(key, value)
|
4
|
+
return false unless value.is_a?(Numeric)
|
5
|
+
self[key] = 0 unless self.has_key?(key)
|
6
|
+
return false unless self[key].is_a?(Numeric)
|
7
|
+
return false unless self[key] < value
|
8
|
+
self[key] = value
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def merge_to_max!(hash)
|
13
|
+
raise ArgumentError unless hash.is_a?(Hash)
|
14
|
+
hash.each do |key, value|
|
15
|
+
self[key] = value unless (self[key] || 0) > value
|
16
|
+
end
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Redisrank
|
2
|
+
class Date
|
3
|
+
|
4
|
+
attr_accessor :year
|
5
|
+
attr_accessor :month
|
6
|
+
attr_accessor :day
|
7
|
+
attr_accessor :hour
|
8
|
+
attr_accessor :min
|
9
|
+
attr_accessor :sec
|
10
|
+
attr_accessor :usec
|
11
|
+
attr_accessor :depth
|
12
|
+
|
13
|
+
DEPTHS = [:year, :month, :day, :hour, :min, :sec, :usec]
|
14
|
+
|
15
|
+
def initialize(input, depth = nil)
|
16
|
+
@depth = depth
|
17
|
+
if input.is_a?(::Time)
|
18
|
+
from_time(input)
|
19
|
+
elsif input.is_a?(::Date)
|
20
|
+
from_date(input)
|
21
|
+
elsif input.is_a?(::String)
|
22
|
+
from_string(input)
|
23
|
+
elsif input.is_a?(::Fixnum)
|
24
|
+
from_integer(input)
|
25
|
+
elsif input.is_a?(::Bignum)
|
26
|
+
from_integer(input)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_t
|
31
|
+
::Time.local(@year, @month, @day, @hour, @min, @sec, @usec)
|
32
|
+
end
|
33
|
+
alias :to_time :to_t
|
34
|
+
|
35
|
+
def to_d
|
36
|
+
::Date.civil(@year, @month, @day)
|
37
|
+
end
|
38
|
+
alias :to_date :to_d
|
39
|
+
|
40
|
+
def to_i
|
41
|
+
to_time.to_i
|
42
|
+
end
|
43
|
+
alias :to_integer :to_i
|
44
|
+
|
45
|
+
def to_s(depth = nil)
|
46
|
+
depth ||= @depth ||= :sec
|
47
|
+
output = ""
|
48
|
+
DEPTHS.each_with_index do |current, i|
|
49
|
+
break if self.send(current).nil?
|
50
|
+
if current != :usec
|
51
|
+
output << self.send(current).to_s.rjust((i <= 0) ? 4 : 2, '0')
|
52
|
+
else
|
53
|
+
output << "." + self.send(current).to_s.rjust(6, '0')
|
54
|
+
end
|
55
|
+
break if current == depth
|
56
|
+
end
|
57
|
+
output
|
58
|
+
end
|
59
|
+
alias :to_string :to_s
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def from_time(input)
|
64
|
+
DEPTHS.each do |k|
|
65
|
+
send("#{k}=", input.send(k))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def from_date(input)
|
70
|
+
[:year, :month, :day].each do |k|
|
71
|
+
send("#{k}=", input.send(k))
|
72
|
+
end
|
73
|
+
[:hour, :min, :sec, :usec].each do |k|
|
74
|
+
send("#{k}=", 0)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def from_integer(input)
|
79
|
+
from_time(::Time.at(input))
|
80
|
+
end
|
81
|
+
|
82
|
+
def from_string(input)
|
83
|
+
input += "19700101000000"[input.size..-1] if input =~ /^\d\d\d[\d]+$/i
|
84
|
+
from_time(::Time.parse(input))
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Redisrank
|
2
|
+
class Event
|
3
|
+
include Database
|
4
|
+
include Options
|
5
|
+
|
6
|
+
attr_reader :id
|
7
|
+
attr_reader :key
|
8
|
+
|
9
|
+
attr_accessor :stats
|
10
|
+
attr_accessor :meta
|
11
|
+
|
12
|
+
def default_options
|
13
|
+
{ :depth => :hour,
|
14
|
+
:store_event => false,
|
15
|
+
:connection_ref => nil,
|
16
|
+
:enable_grouping => true,
|
17
|
+
:label_indexing => true }
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(scope, label = nil, date = nil, stats = {}, opts = {}, meta = {}, is_new = true)
|
21
|
+
parse_options(opts)
|
22
|
+
@key = Key.new(scope, label, date, @options)
|
23
|
+
@stats = stats ||= {}
|
24
|
+
@meta = meta ||= {}
|
25
|
+
@new = is_new
|
26
|
+
end
|
27
|
+
|
28
|
+
def new?
|
29
|
+
@new
|
30
|
+
end
|
31
|
+
|
32
|
+
def date
|
33
|
+
@key.date
|
34
|
+
end
|
35
|
+
|
36
|
+
def date=(input)
|
37
|
+
@key.date = input
|
38
|
+
end
|
39
|
+
|
40
|
+
def scope
|
41
|
+
@key.scope
|
42
|
+
end
|
43
|
+
|
44
|
+
def scope=(input)
|
45
|
+
@key.scope = input
|
46
|
+
end
|
47
|
+
|
48
|
+
def label
|
49
|
+
@key.label
|
50
|
+
end
|
51
|
+
|
52
|
+
def label_hash
|
53
|
+
@key.label_hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def label=(input)
|
57
|
+
@key.label = input
|
58
|
+
end
|
59
|
+
|
60
|
+
def next_id
|
61
|
+
db.incr("#{self.scope}#{KEY_NEXT_ID}")
|
62
|
+
end
|
63
|
+
|
64
|
+
def save
|
65
|
+
return false if !self.new?
|
66
|
+
Summary.update_all(@key, @stats, depth_limit, @options)
|
67
|
+
if @options[:store_event]
|
68
|
+
@id = self.next_id
|
69
|
+
db.hmset("#{self.scope}#{KEY_EVENT}#{@id}",
|
70
|
+
"scope", self.scope,
|
71
|
+
"label", self.label,
|
72
|
+
"date", self.date.to_time.to_s,
|
73
|
+
"stats", self.stats.to_json,
|
74
|
+
"meta", self.meta.to_json,
|
75
|
+
"options", self.options.to_json)
|
76
|
+
db.sadd("#{self.scope}#{KEY_EVENT_IDS}", @id)
|
77
|
+
end
|
78
|
+
@new = false
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def depth_limit
|
83
|
+
@options[:depth] ||= @key.depth
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.create(*args)
|
87
|
+
self.new(*args).save
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.find(scope, id)
|
91
|
+
event = db.hgetall "#{scope}#{KEY_EVENT}#{id}"
|
92
|
+
return nil if event.size == 0
|
93
|
+
self.new( event["scope"], event["label"], event["date"], JSON.parse(event["stats"]),
|
94
|
+
JSON.parse(event["options"]), JSON.parse(event["meta"]), false )
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|