redistat 0.2.6 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +10 -0
- data/lib/redistat.rb +34 -3
- data/lib/redistat/buffer.rb +107 -0
- data/lib/redistat/connection.rb +34 -14
- data/lib/redistat/{database.rb → mixins/database.rb} +0 -0
- data/lib/redistat/{date_helper.rb → mixins/date_helper.rb} +0 -0
- data/lib/redistat/{options.rb → mixins/options.rb} +0 -0
- data/lib/redistat/mixins/synchronize.rb +51 -0
- data/lib/redistat/result.rb +2 -0
- data/lib/redistat/summary.rb +65 -50
- data/lib/redistat/version.rb +1 -1
- data/spec/buffer_spec.rb +157 -0
- data/spec/connection_spec.rb +22 -16
- data/spec/finder_spec.rb +4 -4
- data/spec/model_spec.rb +45 -0
- data/spec/summary_spec.rb +4 -4
- data/spec/synchronize_spec.rb +64 -0
- metadata +14 -8
data/README.md
CHANGED
@@ -25,6 +25,10 @@ view\_stats.rb:
|
|
25
25
|
class ViewStats
|
26
26
|
include Redistat::Model
|
27
27
|
end
|
28
|
+
|
29
|
+
# if using Redistat in multiple threads set this
|
30
|
+
# somewhere in the beginning of the execution stack
|
31
|
+
Redistat.thread_safe = true
|
28
32
|
|
29
33
|
|
30
34
|
### Simple Example ###
|
@@ -187,6 +191,12 @@ By default when fetching statistics, Redistat will figure out how to do the leas
|
|
187
191
|
|
188
192
|
It is also intelligent enough to not fetch each day from 3-31 of a month, instead it would fetch the data for the whole month and the first two days, which are then removed from the summary of the whole month. This means three calls to `hgetall` instead of 29 if each whole day was fetched.
|
189
193
|
|
194
|
+
### Buffer ###
|
195
|
+
|
196
|
+
The buffer is a new, still semi-beta, feature aimed to reduce the number of Redis `hincrby` that Redistat sends. This should only really be useful when you're hitting north of 30,000 Redis requests per second, if your Redis server has limited resources, or against my recommendation you've opted to use 10, 20, or more label grouping levels.
|
197
|
+
|
198
|
+
Buffering tries to fold together multiple `store` calls into as few as possible by merging the statistics hashes from all calls and groups them based on scope, label, date depth, and more. You configure the the buffer by setting `Redistat.buffer_size` to an integer higher than 1. This basically tells Redistat how many `store` calls to buffer in memory before writing all data to Redis.
|
199
|
+
|
190
200
|
|
191
201
|
## Todo ##
|
192
202
|
|
data/lib/redistat.rb
CHANGED
@@ -3,6 +3,7 @@ require 'rubygems'
|
|
3
3
|
require 'date'
|
4
4
|
require 'time'
|
5
5
|
require 'digest/sha1'
|
6
|
+
require 'monitor'
|
6
7
|
|
7
8
|
# Active Support 2.x or 3.x
|
8
9
|
require 'active_support'
|
@@ -15,12 +16,15 @@ require 'time_ext'
|
|
15
16
|
require 'redis'
|
16
17
|
require 'json'
|
17
18
|
|
18
|
-
require 'redistat/options'
|
19
|
+
require 'redistat/mixins/options'
|
20
|
+
require 'redistat/mixins/synchronize'
|
21
|
+
require 'redistat/mixins/database'
|
22
|
+
require 'redistat/mixins/date_helper'
|
23
|
+
|
19
24
|
require 'redistat/connection'
|
20
|
-
require 'redistat/
|
25
|
+
require 'redistat/buffer'
|
21
26
|
require 'redistat/collection'
|
22
27
|
require 'redistat/date'
|
23
|
-
require 'redistat/date_helper'
|
24
28
|
require 'redistat/event'
|
25
29
|
require 'redistat/finder'
|
26
30
|
require 'redistat/key'
|
@@ -33,6 +37,7 @@ require 'redistat/version'
|
|
33
37
|
|
34
38
|
require 'redistat/core_ext'
|
35
39
|
|
40
|
+
|
36
41
|
module Redistat
|
37
42
|
|
38
43
|
KEY_NEXT_ID = ".next_id"
|
@@ -47,6 +52,26 @@ module Redistat
|
|
47
52
|
|
48
53
|
class << self
|
49
54
|
|
55
|
+
def buffer
|
56
|
+
Buffer.instance
|
57
|
+
end
|
58
|
+
|
59
|
+
def buffer_size
|
60
|
+
buffer.size
|
61
|
+
end
|
62
|
+
|
63
|
+
def buffer_size=(size)
|
64
|
+
buffer.size = size
|
65
|
+
end
|
66
|
+
|
67
|
+
def thread_safe
|
68
|
+
Synchronize.thread_safe
|
69
|
+
end
|
70
|
+
|
71
|
+
def thread_safe=(value)
|
72
|
+
Synchronize.thread_safe = value
|
73
|
+
end
|
74
|
+
|
50
75
|
def connection(ref = nil)
|
51
76
|
Connection.get(ref)
|
52
77
|
end
|
@@ -68,3 +93,9 @@ module Redistat
|
|
68
93
|
|
69
94
|
end
|
70
95
|
end
|
96
|
+
|
97
|
+
|
98
|
+
# ensure buffer is flushed on program exit
|
99
|
+
Kernel.at_exit do
|
100
|
+
Redistat.buffer.flush(true)
|
101
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'redistat/core_ext/hash'
|
2
|
+
|
3
|
+
module Redistat
|
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_and_incr!(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
|
+
def buffer_key(key, opts)
|
99
|
+
# depth_limit is not needed as it's evident in key.to_s
|
100
|
+
opts_index = Summary.default_options.keys.sort { |a,b| a.to_s <=> b.to_s }.map do |k|
|
101
|
+
opts[k] if opts.has_key?(k)
|
102
|
+
end
|
103
|
+
"#{key.to_s}:#{opts_index.join(':')}"
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|
data/lib/redistat/connection.rb
CHANGED
@@ -1,29 +1,41 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
1
3
|
module Redistat
|
2
4
|
module Connection
|
3
5
|
|
4
6
|
REQUIRED_SERVER_VERSION = "1.3.10"
|
5
7
|
|
8
|
+
# TODO: Create a ConnectionPool instance object using Sychronize mixin to replace Connection class
|
9
|
+
|
6
10
|
class << self
|
7
11
|
|
12
|
+
# TODO: clean/remove all ref-less connections
|
13
|
+
|
8
14
|
def get(ref = nil)
|
9
15
|
ref ||= :default
|
10
|
-
|
16
|
+
synchronize do
|
17
|
+
connections[references[ref]] || create
|
18
|
+
end
|
11
19
|
end
|
12
20
|
|
13
21
|
def add(conn, ref = nil)
|
14
22
|
ref ||= :default
|
15
|
-
|
16
|
-
|
17
|
-
|
23
|
+
synchronize do
|
24
|
+
check_redis_version(conn)
|
25
|
+
references[ref] = conn.client.id
|
26
|
+
connections[conn.client.id] = conn
|
27
|
+
end
|
18
28
|
end
|
19
29
|
|
20
30
|
def create(options = {})
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
31
|
+
synchronize do
|
32
|
+
options = options.clone
|
33
|
+
ref = options.delete(:ref) || :default
|
34
|
+
options.reverse_merge!(default_options)
|
35
|
+
conn = (connections[connection_id(options)] ||= connection(options))
|
36
|
+
references[ref] = conn.client.id
|
37
|
+
conn
|
38
|
+
end
|
27
39
|
end
|
28
40
|
|
29
41
|
def connections
|
@@ -36,9 +48,12 @@ module Redistat
|
|
36
48
|
|
37
49
|
private
|
38
50
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
51
|
+
def monitor
|
52
|
+
@monitor ||= Monitor.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def synchronize(&block)
|
56
|
+
monitor.synchronize(&block)
|
42
57
|
end
|
43
58
|
|
44
59
|
def connection(options)
|
@@ -46,10 +61,15 @@ module Redistat
|
|
46
61
|
end
|
47
62
|
|
48
63
|
def connection_id(options = {})
|
49
|
-
options.reverse_merge
|
64
|
+
options = options.reverse_merge(default_options)
|
50
65
|
"redis://#{options[:host]}:#{options[:port]}/#{options[:db]}"
|
51
66
|
end
|
52
67
|
|
68
|
+
def check_redis_version(conn)
|
69
|
+
raise RedisServerIsTooOld if conn.info["redis_version"] < REQUIRED_SERVER_VERSION
|
70
|
+
conn
|
71
|
+
end
|
72
|
+
|
53
73
|
def default_options
|
54
74
|
{
|
55
75
|
:host => '127.0.0.1',
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
|
3
|
+
module Redistat
|
4
|
+
module Synchronize
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def included(base)
|
8
|
+
base.send(:include, InstanceMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
def monitor
|
12
|
+
@monitor ||= Monitor.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def thread_safe
|
16
|
+
monitor.synchronize do
|
17
|
+
@thread_safe ||= false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def thread_safe=(value)
|
22
|
+
monitor.synchronize do
|
23
|
+
@thread_safe = value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end # << self
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def thread_safe
|
30
|
+
Synchronize.thread_safe
|
31
|
+
end
|
32
|
+
|
33
|
+
def thread_safe=(value)
|
34
|
+
Synchronize.thread_safe = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def monitor
|
38
|
+
Synchronize.monitor
|
39
|
+
end
|
40
|
+
|
41
|
+
def synchronize(&block)
|
42
|
+
if thread_safe
|
43
|
+
monitor.synchronize(&block)
|
44
|
+
else
|
45
|
+
block.call
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end # InstanceMethods
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
data/lib/redistat/result.rb
CHANGED
data/lib/redistat/summary.rb
CHANGED
@@ -2,66 +2,81 @@ module Redistat
|
|
2
2
|
class Summary
|
3
3
|
include Database
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
:
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
stats ||= {}
|
13
|
-
return nil if stats.size == 0
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def default_options
|
8
|
+
{ :enable_grouping => true,
|
9
|
+
:label_indexing => true,
|
10
|
+
:connection_ref => nil }
|
11
|
+
end
|
14
12
|
|
15
|
-
|
13
|
+
def buffer
|
14
|
+
Redistat.buffer
|
15
|
+
end
|
16
|
+
|
17
|
+
def update_all(key, stats = {}, depth_limit = nil, opts = {})
|
18
|
+
stats ||= {}
|
19
|
+
return if stats.empty?
|
20
|
+
|
21
|
+
options = default_options.merge((opts || {}).reject { |k,v| v.nil? })
|
22
|
+
|
23
|
+
depth_limit ||= key.depth
|
24
|
+
|
25
|
+
update_through_buffer(key, stats, depth_limit, options)
|
26
|
+
end
|
16
27
|
|
17
|
-
|
28
|
+
def update_through_buffer(*args)
|
29
|
+
update(*args) unless buffer.store(*args)
|
30
|
+
end
|
18
31
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
32
|
+
def update(key, stats, depth_limit, opts)
|
33
|
+
if opts[:enable_grouping]
|
34
|
+
stats = inject_group_summaries(stats)
|
35
|
+
key.groups.each do |k|
|
36
|
+
update_key(k, stats, depth_limit, opts[:connection_ref])
|
37
|
+
k.update_index if opts[:label_indexing]
|
38
|
+
end
|
39
|
+
else
|
40
|
+
update_key(key, stats, depth_limit, opts[:connection_ref])
|
24
41
|
end
|
25
|
-
else
|
26
|
-
update_key(key, stats, depth_limit, options[:connection_ref])
|
27
42
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def update_key(key, stats, depth_limit, connection_ref)
|
47
|
+
Date::DEPTHS.each do |depth|
|
48
|
+
update_fields(key, stats, depth, connection_ref)
|
49
|
+
break if depth == depth_limit
|
50
|
+
end
|
36
51
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
52
|
+
|
53
|
+
def update_fields(key, stats, depth, connection_ref = nil)
|
54
|
+
stats.each do |field, value|
|
55
|
+
db(connection_ref).hincrby key.to_s(depth), field, value
|
56
|
+
end
|
42
57
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
58
|
+
|
59
|
+
def inject_group_summaries!(stats)
|
60
|
+
summaries = {}
|
61
|
+
stats.each do |key, value|
|
62
|
+
parts = key.to_s.split(GROUP_SEPARATOR)
|
63
|
+
parts.pop
|
64
|
+
if parts.size > 0
|
65
|
+
sum_parts = []
|
66
|
+
parts.each do |part|
|
67
|
+
sum_parts << part
|
68
|
+
sum_key = sum_parts.join(GROUP_SEPARATOR)
|
69
|
+
(summaries.has_key?(sum_key)) ? summaries[sum_key] += value : summaries[sum_key] = value
|
70
|
+
end
|
56
71
|
end
|
57
72
|
end
|
73
|
+
stats.merge_and_incr!(summaries)
|
58
74
|
end
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
75
|
+
|
76
|
+
def inject_group_summaries(stats)
|
77
|
+
inject_group_summaries!(stats.clone)
|
78
|
+
end
|
79
|
+
|
64
80
|
end
|
65
|
-
|
66
81
|
end
|
67
82
|
end
|
data/lib/redistat/version.rb
CHANGED
data/spec/buffer_spec.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Redistat::Buffer do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@class = Redistat::Buffer
|
7
|
+
@buffer = Redistat::Buffer.instance
|
8
|
+
@key = mock('Key', :to_s => "Scope/label:2011")
|
9
|
+
@stats = {:count => 1, :views => 3}
|
10
|
+
@depth_limit = :hour
|
11
|
+
@opts = {:enable_grouping => true}
|
12
|
+
end
|
13
|
+
|
14
|
+
# let's cleanup after ourselves for the other specs
|
15
|
+
after(:each) do
|
16
|
+
@class.instance_variable_set("@instance", nil)
|
17
|
+
@buffer.size = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should provide instance of itself" do
|
21
|
+
@buffer.should be_a(@class)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should only buffer if buffer size setting is greater than 1" do
|
25
|
+
@buffer.size.should == 0
|
26
|
+
@buffer.send(:should_buffer?).should be_false
|
27
|
+
@buffer.size = 1
|
28
|
+
@buffer.size.should == 1
|
29
|
+
@buffer.send(:should_buffer?).should be_false
|
30
|
+
@buffer.size = 2
|
31
|
+
@buffer.size.should == 2
|
32
|
+
@buffer.send(:should_buffer?).should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should only flush buffer if buffer size is greater than or equal to buffer size setting" do
|
36
|
+
@buffer.size.should == 0
|
37
|
+
@buffer.send(:queue).size.should == 0
|
38
|
+
@buffer.send(:should_flush?).should be_false
|
39
|
+
@buffer.send(:queue)[:hello] = 'world'
|
40
|
+
@buffer.send(:incr_count)
|
41
|
+
@buffer.send(:should_flush?).should be_true
|
42
|
+
@buffer.size = 5
|
43
|
+
@buffer.send(:should_flush?).should be_false
|
44
|
+
3.times { |i|
|
45
|
+
@buffer.send(:queue)[i] = i.to_s
|
46
|
+
@buffer.send(:incr_count)
|
47
|
+
}
|
48
|
+
@buffer.send(:should_flush?).should be_false
|
49
|
+
@buffer.send(:queue)[4] = '4'
|
50
|
+
@buffer.send(:incr_count)
|
51
|
+
@buffer.send(:should_flush?).should be_true
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should force flush queue irregardless of result of #should_flush? when #reset_queue is called with true" do
|
55
|
+
@buffer.send(:queue)[:hello] = 'world'
|
56
|
+
@buffer.send(:incr_count)
|
57
|
+
@buffer.send(:should_flush?).should be_true
|
58
|
+
@buffer.size = 2
|
59
|
+
@buffer.send(:should_flush?).should be_false
|
60
|
+
@buffer.send(:reset_queue).should == {}
|
61
|
+
@buffer.instance_variable_get("@count").should == 1
|
62
|
+
@buffer.send(:reset_queue, true).should == {:hello => 'world'}
|
63
|
+
@buffer.instance_variable_get("@count").should == 0
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should #flush_data into Summary.update properly" do
|
67
|
+
# the root level key value doesn't actually matter, but it's something like this...
|
68
|
+
data = {'ScopeName/label/goes/here:2011::true:true' => {
|
69
|
+
:key => @key,
|
70
|
+
:stats => @stats,
|
71
|
+
:depth_limit => @depth_limit,
|
72
|
+
:opts => @opts
|
73
|
+
}}
|
74
|
+
item = data.first[1]
|
75
|
+
Redistat::Summary.should_receive(:update).with(@key, @stats, @depth_limit, @opts)
|
76
|
+
@buffer.send(:flush_data, data)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should build #buffer_key correctly" do
|
80
|
+
opts = {:enable_grouping => true, :label_indexing => false, :connection_ref => nil}
|
81
|
+
@buffer.send(:buffer_key, @key, opts).should == "#{@key.to_s}::true:false"
|
82
|
+
opts = {:enable_grouping => false, :label_indexing => true, :connection_ref => :omg}
|
83
|
+
@buffer.send(:buffer_key, @key, opts).should == "#{@key.to_s}:omg:false:true"
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "Buffering" do
|
87
|
+
it "should store items on buffer queue" do
|
88
|
+
@buffer.store(@key, @stats, @depth_limit, @opts).should be_false
|
89
|
+
@buffer.size = 5
|
90
|
+
@buffer.store(@key, @stats, @depth_limit, @opts).should be_true
|
91
|
+
@buffer.send(:queue).should have(1).item
|
92
|
+
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:count].should == 1
|
93
|
+
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:views].should == 3
|
94
|
+
@buffer.store(@key, @stats, @depth_limit, @opts).should be_true
|
95
|
+
@buffer.send(:queue).should have(1).items
|
96
|
+
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:count].should == 2
|
97
|
+
@buffer.send(:queue)[@buffer.send(:queue).keys.first][:stats][:views].should == 6
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should flush buffer queue when size is reached" do
|
101
|
+
key = mock('Key', :to_s => "Scope/labelx:2011")
|
102
|
+
@buffer.size = 10
|
103
|
+
Redistat::Summary.should_receive(:update).exactly(2).times.and_return do |k, stats, depth_limit, opts|
|
104
|
+
depth_limit.should == @depth_limit
|
105
|
+
opts.should == @opts
|
106
|
+
if k == @key
|
107
|
+
stats[:count].should == 6
|
108
|
+
stats[:views].should == 18
|
109
|
+
elsif k == key
|
110
|
+
stats[:count].should == 4
|
111
|
+
stats[:views].should == 12
|
112
|
+
end
|
113
|
+
end
|
114
|
+
6.times { @buffer.store(@key, @stats, @depth_limit, @opts).should be_true }
|
115
|
+
4.times { @buffer.store(key, @stats, @depth_limit, @opts).should be_true }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "Thread-Safety" do
|
120
|
+
it "should read/write to buffer queue in a thread-safe manner" do
|
121
|
+
|
122
|
+
# This spec passes wether thread safety is enabled or not. In short I need
|
123
|
+
# better specs for thread-safety, and personally a better understanding of
|
124
|
+
# thread-safety in general.
|
125
|
+
Redistat.thread_safe = true
|
126
|
+
|
127
|
+
key = mock('Key', :to_s => "Scope/labelx:2011")
|
128
|
+
@buffer.size = 100
|
129
|
+
|
130
|
+
Redistat::Summary.should_receive(:update).exactly(2).times.and_return do |k, stats, depth_limit, opts|
|
131
|
+
depth_limit.should == @depth_limit
|
132
|
+
opts.should == @opts
|
133
|
+
if k == @key
|
134
|
+
stats[:count].should == 60
|
135
|
+
stats[:views].should == 180
|
136
|
+
elsif k == key
|
137
|
+
stats[:count].should == 40
|
138
|
+
stats[:views].should == 120
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
threads = []
|
143
|
+
10.times do
|
144
|
+
threads << Thread.new {
|
145
|
+
6.times { @buffer.store(@key, @stats, @depth_limit, @opts).should be_true }
|
146
|
+
4.times { @buffer.store(key, @stats, @depth_limit, @opts).should be_true }
|
147
|
+
}
|
148
|
+
end
|
149
|
+
|
150
|
+
threads.each { |t| t.join }
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should have better specs that actually fail when thread-safety is off"
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
data/spec/connection_spec.rb
CHANGED
@@ -3,33 +3,34 @@ include Redistat
|
|
3
3
|
|
4
4
|
describe Redistat::Connection do
|
5
5
|
|
6
|
+
before(:each) do
|
7
|
+
@redis = Redistat.redis
|
8
|
+
end
|
9
|
+
|
6
10
|
it "should have a valid Redis client instance" do
|
7
11
|
Redistat.redis.should_not be_nil
|
8
12
|
end
|
9
13
|
|
10
14
|
it "should have initialized custom testing connection" do
|
11
|
-
redis
|
12
|
-
redis.client.
|
13
|
-
redis.client.
|
14
|
-
redis.client.db.should == 15
|
15
|
+
@redis.client.host.should == '127.0.0.1'
|
16
|
+
@redis.client.port.should == 8379
|
17
|
+
@redis.client.db.should == 15
|
15
18
|
end
|
16
19
|
|
17
20
|
it "should be able to set and get data" do
|
18
|
-
redis
|
19
|
-
redis.
|
20
|
-
redis.
|
21
|
-
redis.del("hello").should be_true
|
21
|
+
@redis.set("hello", "world")
|
22
|
+
@redis.get("hello").should == "world"
|
23
|
+
@redis.del("hello").should be_true
|
22
24
|
end
|
23
25
|
|
24
26
|
it "should be able to store hashes to Redis" do
|
25
|
-
redis
|
26
|
-
redis.
|
27
|
-
redis.
|
28
|
-
redis.
|
29
|
-
redis.
|
30
|
-
redis.
|
31
|
-
redis.
|
32
|
-
redis.del("hash")
|
27
|
+
@redis.hset("hash", "field", "1")
|
28
|
+
@redis.hget("hash", "field").should == "1"
|
29
|
+
@redis.hincrby("hash", "field", 1)
|
30
|
+
@redis.hget("hash", "field").should == "2"
|
31
|
+
@redis.hincrby("hash", "field", -1)
|
32
|
+
@redis.hget("hash", "field").should == "1"
|
33
|
+
@redis.del("hash")
|
33
34
|
end
|
34
35
|
|
35
36
|
it "should be accessible from Redistat module" do
|
@@ -58,4 +59,9 @@ describe Redistat::Connection do
|
|
58
59
|
Redistat.connect(:port => 8379, :db => 15)
|
59
60
|
end
|
60
61
|
|
62
|
+
# TODO: Test thread-safety
|
63
|
+
it "should be thread-safe" do
|
64
|
+
pending("need to figure out a way to test thread-safety")
|
65
|
+
end
|
66
|
+
|
61
67
|
end
|
data/spec/finder_spec.rb
CHANGED
@@ -194,13 +194,13 @@ describe Redistat::Finder do
|
|
194
194
|
|
195
195
|
def create_example_stats
|
196
196
|
key = Redistat::Key.new(@scope, @label, (first = Time.parse("2010-05-14 13:43")))
|
197
|
-
Redistat::Summary.
|
197
|
+
Redistat::Summary.send(:update_fields, key, @stats, :hour)
|
198
198
|
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 13:53"))
|
199
|
-
Redistat::Summary.
|
199
|
+
Redistat::Summary.send(:update_fields, key, @stats, :hour)
|
200
200
|
key = Redistat::Key.new(@scope, @label, Time.parse("2010-05-14 14:52"))
|
201
|
-
Redistat::Summary.
|
201
|
+
Redistat::Summary.send(:update_fields, key, @stats, :hour)
|
202
202
|
key = Redistat::Key.new(@scope, @label, (last = Time.parse("2010-05-14 15:02")))
|
203
|
-
Redistat::Summary.
|
203
|
+
Redistat::Summary.send(:update_fields, key, @stats, :hour)
|
204
204
|
[first - 1.hour, last + 1.hour]
|
205
205
|
end
|
206
206
|
|
data/spec/model_spec.rb
CHANGED
@@ -156,4 +156,49 @@ describe Redistat::Model do
|
|
156
156
|
stats.total[:weight].should == 617
|
157
157
|
end
|
158
158
|
|
159
|
+
describe "Write Buffer" do
|
160
|
+
before(:each) do
|
161
|
+
Redistat.buffer_size = 20
|
162
|
+
end
|
163
|
+
|
164
|
+
after(:each) do
|
165
|
+
Redistat.buffer_size = 0
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should buffer calls in memory before committing to Redis" do
|
169
|
+
14.times do
|
170
|
+
ModelHelper1.store("sheep.black", {:count => 1, :weight => 461}, @time.hours_ago(4))
|
171
|
+
end
|
172
|
+
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
|
173
|
+
|
174
|
+
5.times do
|
175
|
+
ModelHelper1.store("sheep.black", {:count => 1, :weight => 156}, @time)
|
176
|
+
end
|
177
|
+
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
|
178
|
+
|
179
|
+
ModelHelper1.store("sheep.black", {:count => 1, :weight => 156}, @time)
|
180
|
+
stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
|
181
|
+
stats.total["count"].should == 20
|
182
|
+
stats.total["weight"].should == 7390
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should force flush buffer when #flush(true) is called" do
|
186
|
+
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
|
187
|
+
14.times do
|
188
|
+
ModelHelper1.store("sheep.black", {:count => 1, :weight => 461}, @time.hours_ago(4))
|
189
|
+
end
|
190
|
+
ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1)).total.should == {}
|
191
|
+
Redistat.buffer.flush(true)
|
192
|
+
|
193
|
+
stats = ModelHelper1.fetch("sheep.black", @time.hours_ago(5), @time.hours_since(1))
|
194
|
+
stats.total["count"].should == 14
|
195
|
+
stats.total["weight"].should == 6454
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
159
199
|
end
|
200
|
+
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
|
data/spec/summary_spec.rb
CHANGED
@@ -13,19 +13,19 @@ describe Redistat::Summary do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it "should update a single summary properly" do
|
16
|
-
Redistat::Summary.
|
16
|
+
Redistat::Summary.send(:update_fields, @key, @stats, :hour)
|
17
17
|
summary = db.hgetall(@key.to_s(:hour))
|
18
18
|
summary.should have(2).items
|
19
19
|
summary["views"].should == "3"
|
20
20
|
summary["visitors"].should == "2"
|
21
21
|
|
22
|
-
Redistat::Summary.
|
22
|
+
Redistat::Summary.send(:update_fields, @key, @stats, :hour)
|
23
23
|
summary = db.hgetall(@key.to_s(:hour))
|
24
24
|
summary.should have(2).items
|
25
25
|
summary["views"].should == "6"
|
26
26
|
summary["visitors"].should == "4"
|
27
27
|
|
28
|
-
Redistat::Summary.
|
28
|
+
Redistat::Summary.send(:update_fields, @key, {"views" => -4, "visitors" => -3}, :hour)
|
29
29
|
summary = db.hgetall(@key.to_s(:hour))
|
30
30
|
summary.should have(2).items
|
31
31
|
summary["views"].should == "2"
|
@@ -48,7 +48,7 @@ describe Redistat::Summary do
|
|
48
48
|
|
49
49
|
it "should update summaries even if no label is set" do
|
50
50
|
key = Redistat::Key.new(@scope, nil, @date, {:depth => :day})
|
51
|
-
Redistat::Summary.
|
51
|
+
Redistat::Summary.send(:update_fields, key, @stats, :hour)
|
52
52
|
summary = db.hgetall(key.to_s(:hour))
|
53
53
|
summary.should have(2).items
|
54
54
|
summary["views"].should == "3"
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Redistat::Synchronize do
|
4
|
+
it { should respond_to(:monitor) }
|
5
|
+
it { should respond_to(:thread_safe) }
|
6
|
+
it { should respond_to(:thread_safe=) }
|
7
|
+
|
8
|
+
describe "instanciated class with Redistat::Synchronize included" do
|
9
|
+
subject { SynchronizeSpecHelper.new }
|
10
|
+
it { should respond_to(:monitor) }
|
11
|
+
it { should respond_to(:thread_safe) }
|
12
|
+
it { should respond_to(:thread_safe=) }
|
13
|
+
it { should respond_to(:synchronize) }
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#synchronize method" do
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
Redistat::Synchronize.instance_variable_set("@thread_safe", nil)
|
21
|
+
@obj = SynchronizeSpecHelper.new
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should share single Monitor object across all objects" do
|
25
|
+
@obj.monitor.should == Redistat::Synchronize.monitor
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should share thread_safe option across all objects" do
|
29
|
+
obj2 = SynchronizeSpecHelper.new
|
30
|
+
Redistat::Synchronize.thread_safe.should be_false
|
31
|
+
@obj.thread_safe.should be_false
|
32
|
+
obj2.thread_safe.should be_false
|
33
|
+
@obj.thread_safe = true
|
34
|
+
Redistat::Synchronize.thread_safe.should be_true
|
35
|
+
@obj.thread_safe.should be_true
|
36
|
+
obj2.thread_safe.should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not synchronize when thread_safe is disabled" do
|
40
|
+
# monitor receives :synchronize twice cause #thread_safe is _always_ synchronized
|
41
|
+
Redistat::Synchronize.monitor.should_receive(:synchronize).twice
|
42
|
+
@obj.thread_safe.should be_false # first #synchronize call
|
43
|
+
@obj.synchronize { 'foo' } # one #synchronize call while checking #thread_safe
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should synchronize when thread_safe is enabled" do
|
47
|
+
Monitor.class_eval {
|
48
|
+
# we're stubbing synchronize to ensure it's being called correctly, but still need it :P
|
49
|
+
alias :real_synchronize :synchronize
|
50
|
+
}
|
51
|
+
Redistat::Synchronize.monitor.should_receive(:synchronize).with.exactly(4).times.and_return { |block|
|
52
|
+
Redistat::Synchronize.monitor.real_synchronize(&block)
|
53
|
+
}
|
54
|
+
@obj.thread_safe.should be_false # first synchronize call
|
55
|
+
Redistat::Synchronize.thread_safe = true # second synchronize call
|
56
|
+
@obj.synchronize { 'foo' } # two synchronize calls, once while checking thread_safe, once to call black
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
class SynchronizeSpecHelper
|
63
|
+
include Redistat::Synchronize
|
64
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redistat
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jim Myhrberg
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-04-
|
18
|
+
date: 2011-04-18 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -149,6 +149,7 @@ files:
|
|
149
149
|
- README.md
|
150
150
|
- Rakefile
|
151
151
|
- lib/redistat.rb
|
152
|
+
- lib/redistat/buffer.rb
|
152
153
|
- lib/redistat/collection.rb
|
153
154
|
- lib/redistat/connection.rb
|
154
155
|
- lib/redistat/core_ext.rb
|
@@ -157,21 +158,23 @@ files:
|
|
157
158
|
- lib/redistat/core_ext/fixnum.rb
|
158
159
|
- lib/redistat/core_ext/hash.rb
|
159
160
|
- lib/redistat/core_ext/time.rb
|
160
|
-
- lib/redistat/database.rb
|
161
161
|
- lib/redistat/date.rb
|
162
|
-
- lib/redistat/date_helper.rb
|
163
162
|
- lib/redistat/event.rb
|
164
163
|
- lib/redistat/finder.rb
|
165
164
|
- lib/redistat/finder/date_set.rb
|
166
165
|
- lib/redistat/key.rb
|
167
166
|
- lib/redistat/label.rb
|
167
|
+
- lib/redistat/mixins/database.rb
|
168
|
+
- lib/redistat/mixins/date_helper.rb
|
169
|
+
- lib/redistat/mixins/options.rb
|
170
|
+
- lib/redistat/mixins/synchronize.rb
|
168
171
|
- lib/redistat/model.rb
|
169
|
-
- lib/redistat/options.rb
|
170
172
|
- lib/redistat/result.rb
|
171
173
|
- lib/redistat/scope.rb
|
172
174
|
- lib/redistat/summary.rb
|
173
175
|
- lib/redistat/version.rb
|
174
176
|
- redistat.gemspec
|
177
|
+
- spec/buffer_spec.rb
|
175
178
|
- spec/collection_spec.rb
|
176
179
|
- spec/connection_spec.rb
|
177
180
|
- spec/core_ext/hash_spec.rb
|
@@ -191,6 +194,7 @@ files:
|
|
191
194
|
- spec/scope_spec.rb
|
192
195
|
- spec/spec_helper.rb
|
193
196
|
- spec/summary_spec.rb
|
197
|
+
- spec/synchronize_spec.rb
|
194
198
|
- spec/thread_safety_spec.rb
|
195
199
|
has_rdoc: true
|
196
200
|
homepage: http://github.com/jimeh/redistat
|
@@ -227,6 +231,7 @@ signing_key:
|
|
227
231
|
specification_version: 3
|
228
232
|
summary: A Redis-backed statistics storage and querying library written in Ruby.
|
229
233
|
test_files:
|
234
|
+
- spec/buffer_spec.rb
|
230
235
|
- spec/collection_spec.rb
|
231
236
|
- spec/connection_spec.rb
|
232
237
|
- spec/core_ext/hash_spec.rb
|
@@ -246,4 +251,5 @@ test_files:
|
|
246
251
|
- spec/scope_spec.rb
|
247
252
|
- spec/spec_helper.rb
|
248
253
|
- spec/summary_spec.rb
|
254
|
+
- spec/synchronize_spec.rb
|
249
255
|
- spec/thread_safety_spec.rb
|