redisrank 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|