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 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
 
@@ -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/database'
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
@@ -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
- connections[references[ref]] || create
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
- check_redis_version(conn)
16
- references[ref] = conn.client.id
17
- connections[conn.client.id] = conn
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
- #TODO clean/remove all ref-less connections
22
- ref = options.delete(:ref) || :default
23
- options.reverse_merge!(default_options)
24
- conn = (connections[connection_id(options)] ||= connection(options))
25
- references[ref] = conn.client.id
26
- conn
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 check_redis_version(conn)
40
- raise RedisServerIsTooOld if conn.info["redis_version"] < REQUIRED_SERVER_VERSION
41
- conn
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!(default_options)
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',
@@ -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
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
1
3
  module Redistat
2
4
  class Result < HashWithIndifferentAccess
3
5
 
@@ -2,66 +2,81 @@ module Redistat
2
2
  class Summary
3
3
  include Database
4
4
 
5
- def self.default_options
6
- { :enable_grouping => true,
7
- :label_indexing => true,
8
- :connection_ref => nil }
9
- end
10
-
11
- def self.update_all(key, stats = {}, depth_limit = nil, opts = {})
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
- options = default_options.merge((opts || {}).reject { |k,v| v.nil? })
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
- depth_limit ||= key.depth
28
+ def update_through_buffer(*args)
29
+ update(*args) unless buffer.store(*args)
30
+ end
18
31
 
19
- if options[:enable_grouping]
20
- stats = inject_group_summaries(stats)
21
- key.groups.each do |k|
22
- update_key(k, stats, depth_limit, options[:connection_ref])
23
- k.update_index if options[:label_indexing]
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
- end
29
-
30
- private
31
-
32
- def self.update_key(key, stats, depth_limit, connection_ref)
33
- Date::DEPTHS.each do |depth|
34
- update(key, stats, depth, connection_ref)
35
- break if depth == depth_limit
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
- end
38
-
39
- def self.update(key, stats, depth, connection_ref = nil)
40
- stats.each do |field, value|
41
- db(connection_ref).hincrby key.to_s(depth), field, value
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
- end
44
-
45
- def self.inject_group_summaries!(stats)
46
- summaries = {}
47
- stats.each do |key, value|
48
- parts = key.to_s.split(GROUP_SEPARATOR)
49
- parts.pop
50
- if parts.size > 0
51
- sum_parts = []
52
- parts.each do |part|
53
- sum_parts << part
54
- sum_key = sum_parts.join(GROUP_SEPARATOR)
55
- (summaries.has_key?(sum_key)) ? summaries[sum_key] += value : summaries[sum_key] = value
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
- stats.merge_and_incr!(summaries)
60
- end
61
-
62
- def self.inject_group_summaries(stats)
63
- inject_group_summaries!(stats.clone)
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
@@ -1,3 +1,3 @@
1
1
  module Redistat
2
- VERSION = "0.2.6"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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
@@ -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 = Redistat.redis
12
- redis.client.host.should == '127.0.0.1'
13
- redis.client.port.should == 8379
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 = Redistat.redis
19
- redis.set("hello", "world")
20
- redis.get("hello").should == "world"
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 = Redistat.redis
26
- redis.hset("hash", "field", "1")
27
- redis.hget("hash", "field").should == "1"
28
- redis.hincrby("hash", "field", 1)
29
- redis.hget("hash", "field").should == "2"
30
- redis.hincrby("hash", "field", -1)
31
- redis.hget("hash", "field").should == "1"
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
@@ -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.update(key, @stats, :hour)
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.update(key, @stats, :hour)
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.update(key, @stats, :hour)
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.update(key, @stats, :hour)
203
+ Redistat::Summary.send(:update_fields, key, @stats, :hour)
204
204
  [first - 1.hour, last + 1.hour]
205
205
  end
206
206
 
@@ -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
+
@@ -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.update(@key, @stats, :hour)
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.update(@key, @stats, :hour)
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.update(@key, {"views" => -4, "visitors" => -3}, :hour)
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.update(key, @stats, :hour)
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: 27
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 6
10
- version: 0.2.6
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-13 00:00:00 +01:00
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