aesop 1.1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ class Aesop::Dispatchers::LogDispatcher
2
+ include ::Aesop
3
+
4
+ def dispatch_exception(exception)
5
+ Aesop::Logger.info( exception.class.to_s )
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Aesop
2
+ class RedisConnectionException < Exception; end
3
+ class DispatchException < Exception; end
4
+ class BootloaderException < Exception; end
5
+ class DispatcherLoadException < Exception; end
6
+ class IllegalArgumentException < Exception; end
7
+ end
@@ -0,0 +1,41 @@
1
+ require 'log4r'
2
+
3
+ module Aesop
4
+ module Logger
5
+ DEBUG = 1
6
+ INFO = 2
7
+ WARN = 3
8
+ ERROR = 4
9
+ FATAL = 5
10
+
11
+ class << self
12
+
13
+ DEFAULT_OUTPUT = 'stdout'
14
+
15
+ def log
16
+ @logger ||= setup
17
+ end
18
+
19
+ def setup
20
+ logger = Log4r::Logger.new(configuration.name)
21
+ logger.level = configuration.level
22
+ logger.outputters = configuration.outputters
23
+ logger
24
+ end
25
+
26
+ def reset
27
+ @logger = nil
28
+ end
29
+
30
+ # makes this respond like a Log4r::Logger
31
+ def method_missing(sym, *args, &block)
32
+ log.send sym, *args, &block
33
+ end
34
+
35
+ def configuration
36
+ configatron.logger
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ module Aesop
2
+ class MerbBootLoader < Merb::BootLoader
3
+ after Merb::BootLoader::ChooseAdapter
4
+ def self.run
5
+ Aesop::Aesop.instance.init
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ module Aesop
2
+ module Rails
3
+ class Middleware
4
+ def initialize( app )
5
+ @app = app
6
+ end
7
+
8
+ def call( env )
9
+ begin
10
+ response = @app.call(env)
11
+ rescue Exception => e
12
+ Aesop::Aesop.instance.catch_exception(e)
13
+ end
14
+ response
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Aesop
2
+ class Railtie < ::Rails::Railtie
3
+
4
+ initializer "aesop.start_plugin" do |app|
5
+ Aesop::Aesop.instance.init
6
+
7
+ middleware = if defined?(ActionDispatch::DebugExceptions)
8
+ # Rails >= 3.2.0
9
+ "ActionDispatch::DebugExceptions"
10
+ else
11
+ # Rails < 3.2.0
12
+ "ActionDispatch::ShowExceptions"
13
+ end
14
+
15
+ app.config.middleware.insert_after middleware, "Aesop::Rails::Middleware"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module Aesop
2
+ module Capistrano
3
+ def self.load_into(configuration)
4
+ configuration.load do
5
+ after "deploy:finalize_update", "aesop:record_deployment"
6
+ namespace :aesop do
7
+ desc "Record the current time into a file called DEPLOY_TIME"
8
+ task :record_deployment, :roles => :app do
9
+ set :deployment_time, Time.now.to_i.to_s
10
+ put fetch(:deployment_time), "#{configuration.fetch(:release_path)}/DEPLOY_TIME"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ if cap_config = Capistrano::Configuration.instance
19
+ Aesop::Capistrano.load_into(cap_config)
20
+ end
@@ -0,0 +1,10 @@
1
+ module Aesop
2
+ module Version
3
+ MAJOR = 1
4
+ MINOR = 1
5
+ PATCH = 0
6
+ BUILD = 1
7
+ end
8
+
9
+ VERSION = [Version::MAJOR, Version::MINOR, Version::PATCH, Version::BUILD].compact.join('.')
10
+ end
@@ -0,0 +1,6 @@
1
+ # The capistrano recipes in plugins are automatically
2
+ # loaded from here. From gems, they are available from
3
+ # the lib directory. We have to make them available from
4
+ # both locations
5
+
6
+ require File.join(File.dirname(__FILE__),'..','lib','aesop','recipes.rb')
@@ -0,0 +1,276 @@
1
+ require File.join( File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Aesop::Aesop do
4
+ subject{ Aesop::Aesop.instance }
5
+
6
+ context 'configuration' do
7
+ it 'calls load configuration on initialization' do
8
+ subject.should_receive(:load_configuration)
9
+ bootloader_double = double
10
+ bootloader_double.stub(:boot)
11
+ Aesop::Bootloader.stub(:new).and_return(bootloader_double)
12
+ Aesop::Aesop.instance.init
13
+ end
14
+
15
+ it 'loads default config when no config file exists' do
16
+ File.stub(:exist?).and_return(false)
17
+ config_location = File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'config', 'init.rb'))
18
+ subject.should_receive(:load).with(config_location)
19
+ subject.load_configuration
20
+ end
21
+
22
+ it 'loads config file when it exists' do
23
+ File.stub(:exist?).and_return(true)
24
+ config_location = File.expand_path('config/aesop.rb')
25
+ subject.should_receive(:load).with(config_location)
26
+ subject.load_configuration
27
+ end
28
+
29
+ it 'can receive a block' do
30
+ expect do |block|
31
+ Aesop.configuration(&block)
32
+ end.to yield_with_args(configatron)
33
+ end
34
+
35
+ it 'returns the configuration if no block is given' do
36
+ Aesop.configuration.should == configatron
37
+ end
38
+ end
39
+
40
+ it 'exists' do
41
+ subject.should_not be_nil
42
+ end
43
+
44
+ context 'initialization' do
45
+ it 'boots using the bootloader' do
46
+ Aesop::Bootloader.any_instance.should_receive(:boot)
47
+ subject.init
48
+ end
49
+
50
+ it 'merges the redis password when it is given' do
51
+ password = "pa55word"
52
+ redis_conf = double
53
+ redis_conf.stub(:host)
54
+ redis_conf.stub(:port)
55
+ redis_conf.should_receive(:password).and_return(password)
56
+ subject.configuration.stub(:redis).and_return(redis_conf)
57
+ redis_options = subject.redis_options
58
+ redis_options.should have_key(:password)
59
+ redis_options[:password].should == password
60
+ end
61
+ end
62
+
63
+ context 'exceptions' do
64
+ before :all do
65
+ create_deploy_file(Time.now)
66
+ Aesop::Aesop.instance.init
67
+ end
68
+
69
+ let(:exception){ Exception.new }
70
+ let(:internal_exception){ Aesop::RedisConnectionException.new }
71
+
72
+ before :each do
73
+ subject.redis.stub(:get).and_return(nil)
74
+ subject.redis.stub(:set)
75
+ end
76
+
77
+ it 'raises an RedisConnectionException when a redis exception occurs' do
78
+ subject.instance_variable_set(:@redis, nil)
79
+ Redis.stub(:new){ raise RuntimeError.new }
80
+ lambda{subject.redis}.should raise_error( Aesop::RedisConnectionException )
81
+ end
82
+
83
+ it 'reconnectes when not connected to redis anymore' do
84
+ subject.redis.stub(:connected?).and_return(false)
85
+ expect(Redis).to receive(:new).and_call_original
86
+ subject.redis
87
+ end
88
+
89
+ it 'dispatches each and every exception when an array is provided' do
90
+ exceptions = []
91
+ 5.times do
92
+ exceptions << exception
93
+ end
94
+ subject.should_receive(:catch_exception).with(exception).exactly(5).times
95
+ subject.catch_exceptions( exceptions )
96
+ end
97
+
98
+ it 'throws an error when #catch_exceptions is not called with an array' do
99
+ lambda{ subject.catch_exceptions( exception )}.should raise_error( Aesop::IllegalArgumentException )
100
+ end
101
+
102
+ it 'has a method to to register exceptions' do
103
+ subject.public_methods.map{|m| m.to_s}.should include 'catch_exception'
104
+ end
105
+
106
+ it 'checks to see if an exception should be dispatched' do
107
+ subject.should_receive(:should_dispatch?).with(exception)
108
+ subject.catch_exception( exception )
109
+ end
110
+
111
+ it 'checks if an exception is excluded from dispatching' do
112
+ subject.should_receive(:is_excluded?).with(exception)
113
+ subject.catch_exception( exception )
114
+ end
115
+
116
+ it 'does not dispatch when the exception is excluded' do
117
+ subject.stub(:is_excluded?).and_return(true)
118
+ subject.should_dispatch?(exception).should be_false
119
+ end
120
+
121
+ it 'does dispatch when the exception is not excluded' do
122
+ subject.stub(:is_excluded?).and_return(false)
123
+ subject.should_receive(:within_window?).with(exception).and_return(true)
124
+ subject.should_receive(:retrieve_exception_count).with(exception).and_return(11)
125
+ subject.should_receive(:exception_count_threshold).with(exception).and_return(10)
126
+ subject.should_dispatch?(exception).should be_true
127
+ end
128
+
129
+ it 'reads the excluded exceptions from the configuration' do
130
+ config_double = double("Configuration")
131
+ config_double.should_receive(:excluded_exceptions)
132
+ subject.stub(:configuration){ config_double }
133
+ subject.is_excluded?(exception)
134
+ end
135
+
136
+ it 'ignores exceptions from the excluded list' do
137
+ config_double = double("Configuration")
138
+ config_double.should_receive(:excluded_exceptions).and_return( [ArgumentError] )
139
+ subject.stub(:configuration){ config_double }
140
+ subject.is_excluded?( ArgumentError.new ).should be_true
141
+ end
142
+
143
+ it 'checks if an exception is an internal exception' do
144
+ subject.should_receive(:internal_exception?).with(exception)
145
+ subject.catch_exception( exception )
146
+ end
147
+
148
+ it 'knows when an exception is internal' do
149
+ subject.internal_exception?(exception).should be_false
150
+ end
151
+
152
+ it 'knows when an exception is external' do
153
+ subject.internal_exception?( internal_exception ).should be_true
154
+ end
155
+
156
+ it 'always dispatches Aesop exceptions' do
157
+ subject.stub(:exception_already_dispatched?).and_return(false)
158
+ subject.should_receive(:dispatch_exception).with( internal_exception )
159
+ subject.catch_exception(internal_exception)
160
+ end
161
+
162
+ it 'checks if the exception already occurred' do
163
+ subject.should_receive(:retrieve_exception_count).with(exception)
164
+ subject.should_dispatch?( exception )
165
+ end
166
+
167
+ it 'uses the exception threshold to determine whether the window is open' do
168
+ subject.should_receive(:exception_time_threshold).with(exception).and_return(3600)
169
+ subject.within_window?(exception)
170
+ end
171
+
172
+ it 'checks if the time window is open to dispatch' do
173
+ subject.should_receive(:within_window?).with(exception)
174
+ subject.should_dispatch?( exception )
175
+ end
176
+
177
+ it 'retrieves the deployment time to determine if the exception should be dispatched' do
178
+ subject.should_receive(:retrieve_deployment_time)
179
+ subject.should_dispatch?( exception )
180
+ end
181
+
182
+ it 'uses the exception_prefix when retrieving the exception' do
183
+ subject.should_receive(:exception_prefix).and_return("aesop:exceptions")
184
+ subject.retrieve_exception_count(exception)
185
+ end
186
+
187
+ it 'uses the exception_prefix when storing the exception occurrence' do
188
+ subject.should_receive(:exception_prefix).and_return("aesop:exceptions")
189
+ subject.store_exception_occurrence(exception)
190
+ end
191
+
192
+ it 'uses the exception count threshould when determining whether the occurrence should be dispatched' do
193
+ subject.stub(:within_window?).and_return(true)
194
+ subject.stub(:retrieve_exception_count).and_return(5)
195
+ subject.should_receive(:exception_count_threshold).with(exception).and_return(10)
196
+ subject.should_dispatch?(exception)
197
+ end
198
+
199
+ it 'does not dispatch when the amount of occurrences is smaller than the threshold' do
200
+ subject.should_not_receive(:dispatch_exception)
201
+ subject.should_receive(:within_window?).with(exception).and_return(true)
202
+ subject.should_receive(:retrieve_exception_count).with(exception).and_return(5)
203
+ subject.should_receive(:exception_count_threshold).with(exception).and_return(10)
204
+ subject.catch_exception(exception)
205
+ end
206
+
207
+ it 'dispatches when the amount of occurrences is greater then the threshold' do
208
+ subject.should_receive(:dispatch_exception).with(exception)
209
+ subject.should_receive(:within_window?).with(exception).and_return(true)
210
+ subject.should_receive(:retrieve_exception_count).with(exception).and_return(11)
211
+ subject.should_receive(:exception_count_threshold).with(exception).and_return(10)
212
+ subject.catch_exception(exception)
213
+ end
214
+
215
+ it 'looks up the exception in redis' do
216
+ subject.redis.should_receive(:get).with("aesop:exceptions:#{exception.class.to_s}:count")
217
+ subject.retrieve_exception_count( exception )
218
+ end
219
+
220
+ it 'records the occurrence of the exception while within the window' do
221
+ subject.should_receive(:within_window?).with(exception).and_return(false)
222
+ subject.should_receive(:store_exception_occurrence).with(exception)
223
+ subject.catch_exception(exception)
224
+ end
225
+
226
+ it 'records the occorrence of the exception while outside the window' do
227
+ subject.should_receive(:within_window?).with(exception).and_return(true)
228
+ subject.should_receive(:store_exception_occurrence).with(exception)
229
+ subject.catch_exception(exception)
230
+ end
231
+
232
+ it 'increments the number of occurrences of an exception when stored' do
233
+ subject.redis.should_receive(:incr).with("aesop:exceptions:#{exception.class.to_s}:count")
234
+ subject.catch_exception(exception)
235
+ end
236
+
237
+ it 'checks if the exception has been dispatched yet' do
238
+ subject.stub(:within_window?).and_return(true)
239
+ subject.stub(:retrieve_exception_count).and_return(100)
240
+ subject.stub(:exception_count_treshold).and_return(10)
241
+
242
+ subject.should_receive(:exception_already_dispatched?).with(exception)
243
+ subject.should_dispatch?(exception)
244
+ end
245
+
246
+ it 'does not dispatch the exception when it has already been dispatched' do
247
+ subject.stub(:within_window?).and_return(true)
248
+ subject.stub(:retrieve_exception_count).and_return(100)
249
+ subject.stub(:exception_count_treshold).and_return(10)
250
+
251
+ subject.stub(:exception_already_dispatched?).and_return(true)
252
+ subject.should_dispatch?(exception).should be_false
253
+ end
254
+
255
+ it 'dispatches the exception when it should be dispatched' do
256
+ subject.should_receive(:should_dispatch?).with(exception).and_return(true)
257
+ subject.should_receive(:dispatch_exception).with(exception)
258
+ subject.catch_exception(exception)
259
+ end
260
+
261
+ it 'records the dispatching of the exception' do
262
+ subject.should_receive(:record_exception_dispatch).with(exception)
263
+ subject.dispatch_exception(exception)
264
+ end
265
+
266
+ it 'stores the time of dispatch when an exception is dispatched' do
267
+ subject.redis.should_receive(:set).with("aesop:exceptions:#{exception.class.to_s}:dispatched", anything())
268
+ subject.dispatch_exception(exception)
269
+ end
270
+
271
+ it 'invokes the dispatcher when the exception should be dispatched' do
272
+ Aesop::Dispatcher.instance.should_receive(:dispatch_exception).with(exception)
273
+ subject.dispatch_exception(exception)
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,138 @@
1
+ require File.join( File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Aesop::Bootloader do
4
+ it 'exists' do
5
+ subject.should_not be_nil
6
+ end
7
+
8
+ context 'initialization' do
9
+ it 'attempts to read a DEPLOY file when boot is called' do
10
+ subject.should_receive :read_deploy_time
11
+ subject.boot
12
+ end
13
+
14
+ it 'calls load_dispatchers on boot' do
15
+ subject.should_receive :load_dispatchers
16
+ subject.boot
17
+ end
18
+
19
+ it 'loads all files in the dispatchers directory' do
20
+ dirname = File.dirname( File.join( File.dirname(__FILE__), '..', '..', 'lib', 'aesop', 'dispatchers' ) )
21
+ Dir["#{dirname}/dispatchers/**/*.rb"].each do |file|
22
+ subject.should_receive(:require).with( File.expand_path(file) )
23
+ end
24
+ subject.boot
25
+ end
26
+
27
+ it 'raises an BootloaderException when an exception occurs during bootloading' do
28
+ subject.stub(:determine_latest_deploy_time){ raise RuntimeError.new }
29
+ lambda{ subject.boot }.should raise_error( Aesop::BootloaderException )
30
+ end
31
+
32
+ it 'raises an DispatcherLoadException when an exception occurs during dispatch loading' do
33
+ subject.stub(:require){ raise RuntimeError.new }
34
+ lambda{ subject.load_dispatchers }.should raise_error( Aesop::DispatcherLoadException )
35
+ end
36
+ end
37
+
38
+ context 'reading DEPLOY file' do
39
+ it 'reads the configuration' do
40
+ subject.should_receive(:configuration).at_least(1).and_return(config)
41
+ subject.boot
42
+ end
43
+
44
+ it 'can retrieve the location of the deployment file' do
45
+ subject.deployment_file
46
+ end
47
+
48
+ it 'does not complain when the file does not exist' do
49
+ remove_deploy_file
50
+ deploy_time = subject.read_deploy_time
51
+ deploy_time.should be_nil
52
+ end
53
+
54
+ it 'reads a timestamp from redis' do
55
+ subject.should_receive(:read_current_timestamp)
56
+ subject.boot
57
+ end
58
+
59
+ it 'reads the timestamp from the file' do
60
+ now = Time.now
61
+ create_deploy_file(now)
62
+ subject.read_deploy_time.to_i.should == now.to_i
63
+ end
64
+
65
+ it 'uses #determine_latest_deploy_time to determine the latest deploy time' do
66
+ subject.should_receive(:determine_latest_deploy_time)
67
+ subject.boot
68
+ end
69
+
70
+ it 'determines the latest deploy time by reading deploy file and redis' do
71
+ subject.should_receive(:read_deploy_time)
72
+ subject.should_receive(:read_current_timestamp)
73
+ subject.determine_latest_deploy_time
74
+ end
75
+
76
+ it 'uses the time stored in the file when no time is stored in redis' do
77
+ now = Time.now
78
+ create_deploy_file(now)
79
+ subject.stub(:read_current_timestamp).and_return(0)
80
+ subject.determine_latest_deploy_time.should == now.to_i
81
+ end
82
+
83
+ it 'always stores the latest deploy time to redis' do
84
+ time = Time.now - 500
85
+ newer_time = Time.now
86
+ create_deploy_file(time)
87
+ subject.should_receive(:read_deploy_time).and_return(newer_time.to_i)
88
+ subject.should_receive(:store_timestamp).with(newer_time.to_i)
89
+ subject.boot
90
+ end
91
+
92
+ it 'stores a timestamp when a deployment file exists' do
93
+ create_deploy_file
94
+ subject.should_receive(:store_timestamp)
95
+ subject.boot
96
+ end
97
+
98
+ it 'uses the configuration to lookup the exception prefix' do
99
+ config_double = double()
100
+ config_double.should_receive( :exception_prefix ).and_return('aesop:exceptions')
101
+ subject.stub(:configuration).and_return(config_double)
102
+ subject.reset_exceptions
103
+ end
104
+
105
+ it 'resets all exceptions when #reset_exceptions is called' do
106
+ 5.times do |i|
107
+ subject.redis.set("#{config.exception_prefix}:SomeException#{i}:count", i)
108
+ end
109
+
110
+ subject.reset_exceptions
111
+ subject.redis.keys("#{config.exception_prefix}:*").size.should == 0
112
+ end
113
+
114
+ it 'resets all exceptions when a new deployment has occured' do
115
+ older_time = Time.now - 500
116
+ newer_time = Time.now
117
+ subject.stub(:read_deploy_time).and_return(newer_time.to_i)
118
+ subject.stub(:read_current_timestamp).and_return(older_time.to_i)
119
+ subject.should_receive(:reset_exceptions)
120
+ subject.boot
121
+ end
122
+
123
+ it 'does not rest exceptions when the time in redis is newer' do
124
+ older_time = Time.now - 500
125
+ newer_time = Time.now
126
+ subject.stub(:read_deploy_time).and_return(older_time.to_i)
127
+ subject.stub(:read_current_timestamp).and_return(newer_time.to_i)
128
+ subject.should_not_receive(:reset_exceptions)
129
+ subject.boot
130
+ end
131
+
132
+ it 'stores the timestamp in redis in the aesop:deployment:timestamp key' do
133
+ time = 12345
134
+ subject.redis.should_receive(:set).with(config.deployment_key, time)
135
+ subject.store_timestamp( time )
136
+ end
137
+ end
138
+ end