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.
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