aesop 1.1.0.1

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