redisrank 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/.rspec +2 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +297 -0
  8. data/Rakefile +69 -0
  9. data/lib/redisrank.rb +106 -0
  10. data/lib/redisrank/buffer.rb +110 -0
  11. data/lib/redisrank/collection.rb +20 -0
  12. data/lib/redisrank/connection.rb +89 -0
  13. data/lib/redisrank/core_ext.rb +5 -0
  14. data/lib/redisrank/core_ext/bignum.rb +8 -0
  15. data/lib/redisrank/core_ext/date.rb +8 -0
  16. data/lib/redisrank/core_ext/fixnum.rb +8 -0
  17. data/lib/redisrank/core_ext/hash.rb +20 -0
  18. data/lib/redisrank/core_ext/time.rb +3 -0
  19. data/lib/redisrank/date.rb +88 -0
  20. data/lib/redisrank/event.rb +98 -0
  21. data/lib/redisrank/finder.rb +245 -0
  22. data/lib/redisrank/finder/date_set.rb +99 -0
  23. data/lib/redisrank/key.rb +84 -0
  24. data/lib/redisrank/label.rb +69 -0
  25. data/lib/redisrank/mixins/database.rb +11 -0
  26. data/lib/redisrank/mixins/date_helper.rb +8 -0
  27. data/lib/redisrank/mixins/options.rb +41 -0
  28. data/lib/redisrank/mixins/synchronize.rb +52 -0
  29. data/lib/redisrank/model.rb +77 -0
  30. data/lib/redisrank/result.rb +18 -0
  31. data/lib/redisrank/scope.rb +18 -0
  32. data/lib/redisrank/summary.rb +90 -0
  33. data/lib/redisrank/version.rb +3 -0
  34. data/redisrank.gemspec +31 -0
  35. data/spec/Find Results +3349 -0
  36. data/spec/buffer_spec.rb +104 -0
  37. data/spec/collection_spec.rb +20 -0
  38. data/spec/connection_spec.rb +67 -0
  39. data/spec/core_ext/hash_spec.rb +26 -0
  40. data/spec/database_spec.rb +10 -0
  41. data/spec/date_spec.rb +95 -0
  42. data/spec/event_spec.rb +86 -0
  43. data/spec/finder/date_set_spec.rb +527 -0
  44. data/spec/finder_spec.rb +205 -0
  45. data/spec/key_spec.rb +129 -0
  46. data/spec/label_spec.rb +86 -0
  47. data/spec/model_helper.rb +31 -0
  48. data/spec/model_spec.rb +191 -0
  49. data/spec/options_spec.rb +36 -0
  50. data/spec/redis-test.conf +9 -0
  51. data/spec/result_spec.rb +23 -0
  52. data/spec/scope_spec.rb +27 -0
  53. data/spec/spec_helper.rb +18 -0
  54. data/spec/summary_spec.rb +177 -0
  55. data/spec/synchronize_spec.rb +125 -0
  56. data/spec/thread_safety_spec.rb +39 -0
  57. 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,5 @@
1
+ require 'redisrank/core_ext/bignum'
2
+ require 'redisrank/core_ext/date'
3
+ require 'redisrank/core_ext/fixnum'
4
+ require 'redisrank/core_ext/hash'
5
+ require 'redisrank/core_ext/time'
@@ -0,0 +1,8 @@
1
+ class Bignum
2
+ include Redisrank::DateHelper
3
+
4
+ def to_time
5
+ Time.at(self)
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Date
2
+ include Redisrank::DateHelper
3
+
4
+ def to_time
5
+ Time.parse(self.to_s)
6
+ end
7
+
8
+ end
@@ -0,0 +1,8 @@
1
+ class Fixnum
2
+ include Redisrank::DateHelper
3
+
4
+ def to_time
5
+ Time.at(self)
6
+ end
7
+
8
+ 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,3 @@
1
+ class Time
2
+ include Redisrank::DateHelper
3
+ 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